diff --git a/compiler/src/jdk.graal.compiler/src/jdk/graal/compiler/util/SignatureUtil.java b/compiler/src/jdk.graal.compiler/src/jdk/graal/compiler/util/SignatureUtil.java index 6adff62a5e52..46ae1395f84f 100644 --- a/compiler/src/jdk.graal.compiler/src/jdk/graal/compiler/util/SignatureUtil.java +++ b/compiler/src/jdk.graal.compiler/src/jdk/graal/compiler/util/SignatureUtil.java @@ -42,30 +42,50 @@ private SignatureUtil() { * @return the parsed return type descriptor */ public static String parseSignature(String signature, List parameters) { - if (signature.length() == 0) { - throw new IllegalArgumentException("Signature cannot be empty"); + return parseSignatureInternal(signature, parameters, true, false); + } + + /* + * If throwOnInvalidFormat is not set, returns null if signature parsing failed. + */ + private static String parseSignatureInternal(String signature, List parameters, boolean throwOnInvalidFormat, boolean acceptMissingReturnType) { + if (signature.isEmpty()) { + return throwOrReturn(throwOnInvalidFormat, null, "Signature cannot be empty"); } if (signature.charAt(0) == '(') { int cur = 1; while (cur < signature.length() && signature.charAt(cur) != ')') { - int nextCur = parseSignature(signature, cur); - parameters.add(signature.substring(cur, nextCur)); + int nextCur = parseParameterSignature(signature, cur, throwOnInvalidFormat); + if (nextCur == -1) { + assert !throwOnInvalidFormat : "parseParameterSignature can only return -1 if throwOnInvalidFormat is not set"; + return null; + } + if (parameters != null) { + parameters.add(signature.substring(cur, nextCur)); + } cur = nextCur; } cur++; - int nextCur = parseSignature(signature, cur); + if (acceptMissingReturnType && cur == signature.length()) { + return ""; + } + int nextCur = parseParameterSignature(signature, cur, throwOnInvalidFormat); + if (nextCur == -1) { + assert !throwOnInvalidFormat : "parseParameterSignature can only return -1 if throwOnInvalidFormat is not set"; + return null; + } String returnType = signature.substring(cur, nextCur); if (nextCur != signature.length()) { - throw new IllegalArgumentException("Extra characters at end of signature: " + signature); + return throwOrReturn(throwOnInvalidFormat, null, "Extra characters at end of signature: " + signature); } return returnType; } else { - throw new IllegalArgumentException("Signature must start with a '(': " + signature); + return throwOrReturn(throwOnInvalidFormat, null, "Signature must start with a '(': " + signature); } } - private static int parseSignature(String signature, int start) { + private static int parseParameterSignature(String signature, int start, boolean throwOnInvalidFormat) { try { int cur = start; char first; @@ -78,7 +98,7 @@ private static int parseSignature(String signature, int start) { case 'L': while (signature.charAt(cur) != ';') { if (signature.charAt(cur) == '.') { - throw new IllegalArgumentException("Class name in signature contains '.' at index " + cur + ": " + signature); + return throwOrReturn(throwOnInvalidFormat, -1, "Class name in signature contains '.' at index " + cur + ": " + signature); } cur++; } @@ -95,11 +115,32 @@ private static int parseSignature(String signature, int start) { case 'Z': break; default: - throw new IllegalArgumentException("Invalid character '" + signature.charAt(cur - 1) + "' at index " + (cur - 1) + " in signature: " + signature); + return throwOrReturn(throwOnInvalidFormat, -1, "Invalid character '" + signature.charAt(cur - 1) + "' at index " + (cur - 1) + " in signature: " + signature); } return cur; } catch (StringIndexOutOfBoundsException e) { - throw new IllegalArgumentException("Truncated signature: " + signature); + return throwOrReturn(throwOnInvalidFormat, -1, "Truncated signature: " + signature); + } + } + + /** + * Checks if the given signature can be succesfully parsed by + * {@link #parseSignature(String, List)}. + * + * @param signature the signature to check + * @param acceptMissingReturnType whether a signature without a return type is considered to be + * valid + * @return whether the signature can be successfully parsed + */ + public static boolean isSignatureValid(String signature, boolean acceptMissingReturnType) { + return parseSignatureInternal(signature, null, false, acceptMissingReturnType) != null; + } + + private static T throwOrReturn(boolean shouldThrow, T returnValue, String errorMessage) { + if (shouldThrow) { + throw new IllegalArgumentException(errorMessage); + } else { + return returnValue; } } } diff --git a/sdk/src/org.graalvm.nativeimage/snapshot.sigtest b/sdk/src/org.graalvm.nativeimage/snapshot.sigtest index 2587dbb51122..76d53efedfd7 100644 --- a/sdk/src/org.graalvm.nativeimage/snapshot.sigtest +++ b/sdk/src/org.graalvm.nativeimage/snapshot.sigtest @@ -233,6 +233,15 @@ meth public java.lang.String getElementName() supr java.lang.Error hfds declaringClass,elementName,elementType,parameterTypes,serialVersionUID +CLSS public final org.graalvm.nativeimage.MissingJNIRegistrationError +cons public init(java.lang.String,java.lang.Class,java.lang.Class,java.lang.String,java.lang.String) +meth public java.lang.Class getDeclaringClass() +meth public java.lang.Class getElementType() +meth public java.lang.String getSignature() +meth public java.lang.String getElementName() +supr java.lang.Error +hfds declaringClass,elementName,elementType,signature,serialVersionUID + CLSS public abstract interface org.graalvm.nativeimage.ObjectHandle intf org.graalvm.word.ComparableWord diff --git a/sdk/src/org.graalvm.nativeimage/src/org/graalvm/nativeimage/MissingJNIRegistrationError.java b/sdk/src/org.graalvm.nativeimage/src/org/graalvm/nativeimage/MissingJNIRegistrationError.java new file mode 100644 index 000000000000..0504f27ddc75 --- /dev/null +++ b/sdk/src/org.graalvm.nativeimage/src/org/graalvm/nativeimage/MissingJNIRegistrationError.java @@ -0,0 +1,132 @@ +/* + * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * The Universal Permissive License (UPL), Version 1.0 + * + * Subject to the condition set forth below, permission is hereby granted to any + * person obtaining a copy of this software, associated documentation and/or + * data (collectively the "Software"), free of charge and under any and all + * copyright rights in the Software, and any and all patent rights owned or + * freely licensable by each licensor hereunder covering either (i) the + * unmodified Software as contributed to or provided by such licensor, or (ii) + * the Larger Works (as defined below), to deal in both + * + * (a) the Software, and + * + * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if + * one is included with the Software each a "Larger Work" to which the Software + * is contributed by such licensors), + * + * without restriction, including without limitation the rights to copy, create + * derivative works of, display, perform, and distribute the Software and make, + * use, sell, offer for sale, import, export, have made, and have sold the + * Software and the Larger Work(s), and to sublicense the foregoing rights on + * either these or other terms. + * + * This license is subject to the following condition: + * + * The above copyright notice and either this complete permission notice or at a + * minimum a reference to the UPL must be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package org.graalvm.nativeimage; + +import java.io.Serial; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Method; + +/** + * This exception is thrown when a JNI query tries to access an element that was not + * registered + * for JNI access in the program. When an element is not registered, the exception will be + * thrown both for elements that exist and elements that do not exist on the given classpath. + *

+ * The purpose of this exception is to easily discover unregistered elements and to assure that all + * JNI operations for registered elements have the expected behaviour. + *

+ * Queries will succeed (or throw the expected error) if the element was registered for JNI access. + * If that is not the case, a {@link MissingJNIRegistrationError} will be thrown. + *

+ * The exception thrown by the JNI query is a pending exception that needs to be explicitly + * checked by the calling native code. + * + * Examples: + *

+ * Registration: {@code "fields": [{"name": "registeredField"}, {"name": + * "registeredNonexistentField"}]}
+ * {@code GetFieldID(declaringClass, "registeredField")} will return the expected field.
+ * {@code GetFieldID(declaringClass, "registeredNonexistentField")} will throw a + * {@link NoSuchFieldError}.
+ * {@code GetFieldID(declaringClass, "unregisteredField")} will throw a + * {@link MissingJNIRegistrationError}.
+ * {@code GetFieldID(declaringClass, "unregisteredNonexistentField")} will throw a + * {@link MissingJNIRegistrationError}.
+ * + * @since 24.1 + */ +public final class MissingJNIRegistrationError extends Error { + @Serial private static final long serialVersionUID = -8940056537864516986L; + + private final Class elementType; + + private final Class declaringClass; + + private final String elementName; + + private final String signature; + + /** + * @since 24.1 + */ + public MissingJNIRegistrationError(String message, Class elementType, Class declaringClass, String elementName, String signature) { + super(message); + this.elementType = elementType; + this.declaringClass = declaringClass; + this.elementName = elementName; + this.signature = signature; + } + + /** + * @return The type of the element trying to be queried ({@link Class}, {@link Method}, + * {@link Field} or {@link Constructor}). + * @since 23.0 + */ + public Class getElementType() { + return elementType; + } + + /** + * @return The class on which the missing query was tried, or null on static queries. + * @since 23.0 + */ + public Class getDeclaringClass() { + return declaringClass; + } + + /** + * @return The name of the queried element. + * @since 23.0 + */ + public String getElementName() { + return elementName; + } + + /** + * @return The signature passed to the query, or null if the query doesn't take a signature as + * argument. + * @since 23.0 + */ + public String getSignature() { + return signature; + } +} diff --git a/sdk/src/org.graalvm.nativeimage/src/org/graalvm/nativeimage/impl/ReflectionRegistry.java b/sdk/src/org.graalvm.nativeimage/src/org/graalvm/nativeimage/impl/ReflectionRegistry.java index 4198a895ec06..9d0534347cfb 100644 --- a/sdk/src/org.graalvm.nativeimage/src/org/graalvm/nativeimage/impl/ReflectionRegistry.java +++ b/sdk/src/org.graalvm.nativeimage/src/org/graalvm/nativeimage/impl/ReflectionRegistry.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2017, 2024, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * The Universal Permissive License (UPL), Version 1.0 @@ -55,4 +55,11 @@ default void register(ConfigurationCondition condition, Class... classes) { void register(ConfigurationCondition condition, boolean finalIsWritable, Field... fields); + void registerClassLookup(ConfigurationCondition condition, String typeName); + + void registerFieldLookup(ConfigurationCondition condition, Class declaringClass, String fieldName); + + void registerMethodLookup(ConfigurationCondition condition, Class declaringClass, String methodName, Class... parameterTypes); + + void registerConstructorLookup(ConfigurationCondition condition, Class declaringClass, Class... parameterTypes); } diff --git a/sdk/src/org.graalvm.nativeimage/src/org/graalvm/nativeimage/impl/RuntimeReflectionSupport.java b/sdk/src/org.graalvm.nativeimage/src/org/graalvm/nativeimage/impl/RuntimeReflectionSupport.java index 416b5fe7cdbd..68e78b2c975b 100644 --- a/sdk/src/org.graalvm.nativeimage/src/org/graalvm/nativeimage/impl/RuntimeReflectionSupport.java +++ b/sdk/src/org.graalvm.nativeimage/src/org/graalvm/nativeimage/impl/RuntimeReflectionSupport.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2017, 2024, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * The Universal Permissive License (UPL), Version 1.0 @@ -67,12 +67,4 @@ public interface RuntimeReflectionSupport extends ReflectionRegistry { void registerAllSignersQuery(ConfigurationCondition condition, Class clazz); void registerClassLookupException(ConfigurationCondition condition, String typeName, Throwable t); - - void registerClassLookup(ConfigurationCondition condition, String typeName); - - void registerFieldLookup(ConfigurationCondition condition, Class declaringClass, String fieldName); - - void registerMethodLookup(ConfigurationCondition condition, Class declaringClass, String methodName, Class... parameterTypes); - - void registerConstructorLookup(ConfigurationCondition condition, Class declaringClass, Class... parameterTypes); } diff --git a/substratevm/CHANGELOG.md b/substratevm/CHANGELOG.md index 9d8cafb85078..d60d6ccc61e2 100644 --- a/substratevm/CHANGELOG.md +++ b/substratevm/CHANGELOG.md @@ -27,6 +27,7 @@ This changelog summarizes major changes to GraalVM Native Image. * (GR-18214) In-place compacting garbage collection for the Serial GC old generation with `-H:+CompactingOldGen`. * (GR-52844) Add `Os` a new optimization mode to configure the optimizer in a way to get the smallest code size. * (GR-49770) Add support for glob patterns in resource-config files in addition to regexp. The Tracing agent now prints entries in the glob format. +* (GR-46386) Throw missing registration errors for JNI queries when the query was not included in the reachability metadata. ## GraalVM for JDK 22 (Internal Version 24.0.0) * (GR-48304) Red Hat added support for the JFR event ThreadAllocationStatistics. diff --git a/substratevm/src/com.oracle.svm.agent/src/com/oracle/svm/agent/JniCallInterceptor.java b/substratevm/src/com.oracle.svm.agent/src/com/oracle/svm/agent/JniCallInterceptor.java index 195e2b8d3181..030a943980f7 100644 --- a/substratevm/src/com.oracle.svm.agent/src/com/oracle/svm/agent/JniCallInterceptor.java +++ b/substratevm/src/com.oracle.svm.agent/src/com/oracle/svm/agent/JniCallInterceptor.java @@ -53,7 +53,6 @@ import com.oracle.svm.agent.tracing.core.Tracer; import com.oracle.svm.core.c.function.CEntryPointOptions; import com.oracle.svm.core.jni.headers.JNIEnvironment; -import com.oracle.svm.core.jni.headers.JNIErrors; import com.oracle.svm.core.jni.headers.JNIFieldId; import com.oracle.svm.core.jni.headers.JNIFunctionPointerTypes.DefineClassFunctionPointer; import com.oracle.svm.core.jni.headers.JNIFunctionPointerTypes.FindClassFunctionPointer; @@ -115,7 +114,7 @@ private static JNIObjectHandle defineClass(JNIEnvironment env, CCharPointer name JNIObjectHandle callerClass = getCallerClass(state, env); JNIObjectHandle result = jniFunctions().getDefineClass().invoke(env, name, loader, buf, bufLen); if (shouldTrace()) { - traceCall(env, "DefineClass", nullHandle(), nullHandle(), callerClass, result.notEqual(nullHandle()), state, fromCString(name)); + traceCall(env, "DefineClass", nullHandle(), nullHandle(), callerClass, name.notEqual(nullHandle()), state, fromCString(name)); } return result; } @@ -138,7 +137,7 @@ private static JNIObjectHandle findClass(JNIEnvironment env, CCharPointer name) result = nullHandle(); } if (shouldTrace()) { - traceCall(env, "FindClass", nullHandle(), nullHandle(), callerClass, result.notEqual(nullHandle()), state, fromCString(name)); + traceCall(env, "FindClass", nullHandle(), nullHandle(), callerClass, name.notEqual(nullHandle()), state, fromCString(name)); } return result; } @@ -153,7 +152,7 @@ static JNIObjectHandle allocObject(JNIEnvironment env, JNIObjectHandle clazz) { result = nullHandle(); } if (shouldTrace()) { - traceCall(env, "AllocObject", clazz, nullHandle(), callerClass, result.notEqual(nullHandle()), state); + traceCall(env, "AllocObject", clazz, nullHandle(), callerClass, clazz.notEqual(nullHandle()), state); } return result; @@ -166,7 +165,8 @@ private static JNIMethodId getMethodID(JNIEnvironment env, JNIObjectHandle clazz JNIObjectHandle callerClass = getCallerClass(state, env); JNIMethodId result = jniFunctions().getGetMethodID().invoke(env, clazz, name, signature); if (shouldTrace()) { - traceCall(env, "GetMethodID", clazz, getMethodDeclaringClass(result), callerClass, result.isNonNull(), state, fromCString(name), fromCString(signature)); + boolean shouldHandleCall = clazz.notEqual(nullHandle()) && name.notEqual(nullHandle()) && signature.notEqual(nullHandle()); + traceCall(env, "GetMethodID", clazz, getMethodDeclaringClass(result), callerClass, shouldHandleCall, state, fromCString(name), fromCString(signature)); } return result; } @@ -177,9 +177,9 @@ private static JNIMethodId getStaticMethodID(JNIEnvironment env, JNIObjectHandle InterceptedState state = initInterceptedState(); JNIObjectHandle callerClass = getCallerClass(state, env); JNIMethodId result = jniFunctions().getGetStaticMethodID().invoke(env, clazz, name, signature); - result.isNonNull(); if (shouldTrace()) { - traceCall(env, "GetStaticMethodID", clazz, getMethodDeclaringClass(result), callerClass, result.isNonNull(), state, fromCString(name), fromCString(signature)); + boolean shouldHandleCall = clazz.notEqual(nullHandle()) && name.notEqual(nullHandle()) && signature.notEqual(nullHandle()); + traceCall(env, "GetStaticMethodID", clazz, getMethodDeclaringClass(result), callerClass, shouldHandleCall, state, fromCString(name), fromCString(signature)); } return result; } @@ -191,7 +191,8 @@ private static JNIFieldId getFieldID(JNIEnvironment env, JNIObjectHandle clazz, JNIObjectHandle callerClass = getCallerClass(state, env); JNIFieldId result = jniFunctions().getGetFieldID().invoke(env, clazz, name, signature); if (shouldTrace()) { - traceCall(env, "GetFieldID", clazz, getFieldDeclaringClass(clazz, result), callerClass, result.isNonNull(), state, fromCString(name), fromCString(signature)); + boolean shouldHandleCall = clazz.notEqual(nullHandle()) && name.notEqual(nullHandle()) && signature.notEqual(nullHandle()); + traceCall(env, "GetFieldID", clazz, getFieldDeclaringClass(clazz, result), callerClass, shouldHandleCall, state, fromCString(name), fromCString(signature)); } return result; } @@ -203,7 +204,8 @@ private static JNIFieldId getStaticFieldID(JNIEnvironment env, JNIObjectHandle c JNIObjectHandle callerClass = getCallerClass(state, env); JNIFieldId result = jniFunctions().getGetStaticFieldID().invoke(env, clazz, name, signature); if (shouldTrace()) { - traceCall(env, "GetStaticFieldID", clazz, getFieldDeclaringClass(clazz, result), callerClass, result.isNonNull(), state, fromCString(name), fromCString(signature)); + boolean shouldHandleCall = clazz.notEqual(nullHandle()) && name.notEqual(nullHandle()) && signature.notEqual(nullHandle()); + traceCall(env, "GetStaticFieldID", clazz, getFieldDeclaringClass(clazz, result), callerClass, shouldHandleCall, state, fromCString(name), fromCString(signature)); } return result; } @@ -215,7 +217,7 @@ private static int throwNew(JNIEnvironment env, JNIObjectHandle clazz, CCharPoin JNIObjectHandle callerClass = getCallerClass(state, env); int result = jniFunctions().getThrowNew().invoke(env, clazz, message); if (shouldTrace()) { - traceCall(env, "ThrowNew", clazz, nullHandle(), callerClass, (result == JNIErrors.JNI_OK()), state, Tracer.UNKNOWN_VALUE); + traceCall(env, "ThrowNew", clazz, nullHandle(), callerClass, clazz.notEqual(nullHandle()), state, Tracer.UNKNOWN_VALUE); } return result; } @@ -282,7 +284,8 @@ private static JNIObjectHandle toReflectedMethod(JNIEnvironment env, JNIObjectHa } JNIObjectHandle result = jniFunctions().getToReflectedMethod().invoke(env, clazz, method, isStatic); if (shouldTrace()) { - traceCall(env, "ToReflectedMethod", clazz, declaring, callerClass, result.notEqual(nullHandle()), state, name, signature); + boolean shouldHandleCall = clazz.notEqual(nullHandle()) && name != null && signature != null; + traceCall(env, "ToReflectedMethod", clazz, declaring, callerClass, shouldHandleCall, state, name, signature); } return result; } @@ -296,7 +299,8 @@ private static JNIObjectHandle toReflectedField(JNIEnvironment env, JNIObjectHan String name = getFieldName(clazz, field); JNIObjectHandle result = jniFunctions().getToReflectedField().invoke(env, clazz, field, isStatic); if (shouldTrace()) { - traceCall(env, "ToReflectedField", clazz, declaring, callerClass, result.notEqual(nullHandle()), state, name); + boolean shouldHandleCall = clazz.notEqual(nullHandle()) && name != null; + traceCall(env, "ToReflectedField", clazz, declaring, callerClass, shouldHandleCall, state, name); } return result; } @@ -315,7 +319,7 @@ private static JNIObjectHandle newObjectArray(JNIEnvironment env, int length, JN } } if (shouldTrace()) { - traceCall(env, "NewObjectArray", resultClass, nullHandle(), callerClass, result.notEqual(nullHandle()), state); + traceCall(env, "NewObjectArray", resultClass, nullHandle(), callerClass, elementClass.notEqual(nullHandle()), state); } return result; } diff --git a/substratevm/src/com.oracle.svm.agent/src/com/oracle/svm/agent/tracing/core/Tracer.java b/substratevm/src/com.oracle.svm.agent/src/com/oracle/svm/agent/tracing/core/Tracer.java index 01669919c4be..2737ab2a0dfc 100644 --- a/substratevm/src/com.oracle.svm.agent/src/com/oracle/svm/agent/tracing/core/Tracer.java +++ b/substratevm/src/com.oracle.svm.agent/src/com/oracle/svm/agent/tracing/core/Tracer.java @@ -86,7 +86,8 @@ public void tracePhaseChange(String phase) { * @param declaringClass If the traced call resolves a member of {@code clazz}, this can be * specified to provide the (super)class which actually declares that member. * @param callerClass The class on the call stack which performed the call. - * @param result The result of the call. + * @param result The result of the call or an indication on whether to handle the call if the + * result is not required in the processor. * @param stackTrace Full stack trace leading to (and including) the call, or null if not * available. The first element of this array represents the top of the stack. * diff --git a/substratevm/src/com.oracle.svm.configure/src/com/oracle/svm/configure/config/ConfigurationType.java b/substratevm/src/com.oracle.svm.configure/src/com/oracle/svm/configure/config/ConfigurationType.java index 0bb5e209c31f..f1503250f941 100644 --- a/substratevm/src/com.oracle.svm.configure/src/com/oracle/svm/configure/config/ConfigurationType.java +++ b/substratevm/src/com.oracle.svm.configure/src/com/oracle/svm/configure/config/ConfigurationType.java @@ -46,6 +46,8 @@ import jdk.graal.compiler.util.json.JsonPrinter; import jdk.graal.compiler.util.json.JsonWriter; +import jdk.graal.compiler.util.SignatureUtil; + /** * Type usage information, part of a {@link TypeConfiguration}. Unlike other configuration classes * like {@link ConfigurationMethod}, this class is not immutable and uses locking to synchronize @@ -334,6 +336,17 @@ public void addMethod(String name, String internalSignature, ConfigurationMember public synchronized void addMethod(String name, String internalSignature, ConfigurationMemberDeclaration declaration, ConfigurationMemberAccessibility accessibility) { ConfigurationMemberInfo kind = ConfigurationMemberInfo.get(declaration, accessibility); boolean matchesAllSignatures = (internalSignature == null); + if (!matchesAllSignatures) { + /* + * A method with an invalid signature will not match any existing method. The signature + * is also checked during run-time queries (in this case, JNI's `Get(Static)MethodID`) + * and the missing registration error check does not happen if the signature is invalid, + * so there is no need to register a negative query for the method either. + */ + if (!SignatureUtil.isSignatureValid(internalSignature, true)) { + return; + } + } if (ConfigurationMethod.isConstructorName(name) ? hasAllConstructors(declaration, accessibility) : hasAllMethods(declaration, accessibility)) { if (!matchesAllSignatures) { if (accessibility == ConfigurationMemberAccessibility.ACCESSED) { diff --git a/substratevm/src/com.oracle.svm.configure/src/com/oracle/svm/configure/config/SignatureUtil.java b/substratevm/src/com.oracle.svm.configure/src/com/oracle/svm/configure/config/SignatureUtil.java index 216c9345dca7..7e4ee9489823 100644 --- a/substratevm/src/com.oracle.svm.configure/src/com/oracle/svm/configure/config/SignatureUtil.java +++ b/substratevm/src/com.oracle.svm.configure/src/com/oracle/svm/configure/config/SignatureUtil.java @@ -35,7 +35,13 @@ import jdk.vm.ci.meta.MetaUtil; public class SignatureUtil { + + public static final int MIN_SIGNATURE_LENGTH = "()".length(); + public static String[] toParameterTypes(String signature) { + if (signature.length() < MIN_SIGNATURE_LENGTH) { + throw new IllegalArgumentException("Signature too short: " + signature); + } List list = new ArrayList<>(); int position = 1; int arrayDimensions = 0; diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jni/MissingJNIRegistrationUtils.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jni/MissingJNIRegistrationUtils.java new file mode 100644 index 000000000000..bf11ba771374 --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jni/MissingJNIRegistrationUtils.java @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2024, 2024, 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.jni; + +import static com.oracle.svm.core.MissingRegistrationUtils.ERROR_EMPHASIS_INDENT; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; + +import org.graalvm.nativeimage.MissingJNIRegistrationError; + +import com.oracle.svm.core.MissingRegistrationUtils; + +public final class MissingJNIRegistrationUtils { + + public static void forClass(String className) { + MissingJNIRegistrationError exception = new MissingJNIRegistrationError(errorMessage("access class", className), + Class.class, null, className, null); + report(exception); + } + + public static void forField(Class declaringClass, String fieldName) { + MissingJNIRegistrationError exception = new MissingJNIRegistrationError(errorMessage("access field", + declaringClass.getTypeName() + "#" + fieldName), + Field.class, declaringClass, fieldName, null); + report(exception); + } + + public static void forMethod(Class declaringClass, String methodName, String signature) { + MissingJNIRegistrationError exception = new MissingJNIRegistrationError(errorMessage("access method", + declaringClass.getTypeName() + "#" + methodName + signature), + Method.class, declaringClass, methodName, signature); + report(exception); + } + + private static String errorMessage(String failedAction, String elementDescriptor) { + return errorMessage(failedAction, elementDescriptor, null, "JNI"); + } + + private static String errorMessage(String failedAction, String elementDescriptor, String note, String helpLink) { + /* 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 JNI access. 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."; + } + + private static void report(MissingJNIRegistrationError exception) { + // GR-54504: get responsible class from anchor + MissingRegistrationUtils.report(exception, null); + } +} diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jni/access/JNIAccessibleClass.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jni/access/JNIAccessibleClass.java index 4889c1e98a97..8f93b1a2dcfa 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jni/access/JNIAccessibleClass.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jni/access/JNIAccessibleClass.java @@ -33,6 +33,7 @@ import org.graalvm.nativeimage.Platforms; import com.oracle.svm.core.util.ImageHeapMap; +import com.oracle.svm.core.util.VMError; import jdk.vm.ci.meta.MetaUtil; @@ -50,6 +51,16 @@ public JNIAccessibleClass(Class clazz) { this.classObject = clazz; } + @Platforms(HOSTED_ONLY.class) + JNIAccessibleClass() { + /* For negative queries */ + this.classObject = null; + } + + public boolean isNegative() { + return classObject == null; + } + public Class getClassObject() { return classObject; } @@ -87,7 +98,23 @@ public MapCursor getMethods( } public JNIAccessibleMethod getMethod(JNIAccessibleMethodDescriptor descriptor) { - return (methods != null) ? methods.get(descriptor) : null; + if (methods == null) { + return null; + } + JNIAccessibleMethod method = methods.get(descriptor); + if (method == null) { + /* + * Negative method queries match any return type and are stored with only parameter + * types in their signature. + */ + String signatureWithoutReturnType = descriptor.getSignatureWithoutReturnType(); + if (signatureWithoutReturnType != null) { + /* We only need to perform the lookup on valid signatures */ + method = methods.get(new JNIAccessibleMethodDescriptor(descriptor.getNameConvertToString(), signatureWithoutReturnType)); + VMError.guarantee(method == null || method.isNegative(), "Only negative queries should have a signature without return type"); + } + } + return method; } String getInternalName() { diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jni/access/JNIAccessibleField.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jni/access/JNIAccessibleField.java index fc3cfbac3820..d158f3d104de 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jni/access/JNIAccessibleField.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jni/access/JNIAccessibleField.java @@ -49,8 +49,14 @@ public final class JNIAccessibleField extends JNIAccessibleMember { private static final UnsignedWord ID_STATIC_FLAG = WordFactory.unsigned(-1L).unsignedShiftRight(1).add(1); /* 01000000...0 */ private static final UnsignedWord ID_OBJECT_FLAG = ID_STATIC_FLAG.unsignedShiftRight(1); + /* 00100000...0 */ + private static final UnsignedWord ID_NEGATIVE_FLAG = ID_OBJECT_FLAG.unsignedShiftRight(1); /* 00111111...1 */ - private static final UnsignedWord ID_OFFSET_MASK = ID_OBJECT_FLAG.subtract(1); + private static final UnsignedWord ID_OFFSET_MASK = ID_NEGATIVE_FLAG.subtract(1); + + public static JNIAccessibleField negativeFieldQuery(JNIAccessibleClass jniClass) { + return new JNIAccessibleField(jniClass, null, 0); + } /** * For instance fields, the offset of the field in an object of the @@ -75,7 +81,8 @@ public static WordBase getOffsetFromId(JNIFieldId id) { *

    *
  • 1 bit for a flag indicating whether the field is static
  • *
  • 1 bit for a flag indicating whether the field is an object reference
  • - *
  • Remaining 62 bits for (unsigned) offset in the object
  • + *
  • 1 bit for a flag indicating whether the field is a negative query
  • + *
  • Remaining 61 bits for (unsigned) offset in the object
  • *
*/ @UnknownPrimitiveField(availability = ReadyForCompilation.class)// @@ -86,7 +93,9 @@ public JNIAccessibleField(JNIAccessibleClass declaringClass, JavaKind kind, int super(declaringClass); UnsignedWord bits = Modifier.isStatic(modifiers) ? ID_STATIC_FLAG : WordFactory.zero(); - if (kind.isObject()) { + if (kind == null) { + bits = bits.or(ID_NEGATIVE_FLAG); + } else if (kind.isObject()) { bits = bits.or(ID_OBJECT_FLAG); } this.flags = bits; @@ -101,12 +110,22 @@ public boolean isStatic() { return id.and(ID_STATIC_FLAG).notEqual(0); } + @Platforms(HOSTED_ONLY.class) + public boolean isNegativeHosted() { + return flags.and(ID_NEGATIVE_FLAG).notEqual(0); + } + + public boolean isNegative() { + assert !id.equal(0); + return id.and(ID_NEGATIVE_FLAG).notEqual(0); + } + @Platforms(HOSTED_ONLY.class) public void finishBeforeCompilation(int offset, EconomicSet> hidingSubclasses) { assert id.equal(0); - assert ID_OFFSET_MASK.and(offset).equal(offset) : "Offset is too large to be encoded in the JNIAccessibleField ID"; + assert isNegativeHosted() || ID_OFFSET_MASK.and(offset).equal(offset) : "Offset is too large to be encoded in the JNIAccessibleField ID"; - id = flags.or(offset); + id = isNegativeHosted() ? flags : flags.or(offset); setHidingSubclasses(hidingSubclasses); } } diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jni/access/JNIAccessibleMethod.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jni/access/JNIAccessibleMethod.java index 7d3fcc16aded..e7f22228b31d 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jni/access/JNIAccessibleMethod.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jni/access/JNIAccessibleMethod.java @@ -37,6 +37,7 @@ import com.oracle.svm.core.BuildPhaseProvider.ReadyForCompilation; import com.oracle.svm.core.SubstrateOptions; import com.oracle.svm.core.Uninterruptible; +import com.oracle.svm.core.code.RuntimeMetadataDecoderImpl; import com.oracle.svm.core.graal.nodes.LoadOpenTypeWorldDispatchTableStartingOffset; import com.oracle.svm.core.heap.UnknownPrimitiveField; import com.oracle.svm.core.jni.CallVariant; @@ -59,6 +60,10 @@ public final class JNIAccessibleMethod extends JNIAccessibleMember { public static final int INTERFACE_TYPEID_UNNEEDED = -3; public static final int NEW_OBJECT_INVALID_FOR_ABSTRACT_TYPE = -1; + public static JNIAccessibleMethod negativeMethodQuery(JNIAccessibleClass jniClass) { + return new JNIAccessibleMethod(jniClass, RuntimeMetadataDecoderImpl.NEGATIVE_FLAG_MASK); + } + @Platforms(HOSTED_ONLY.class) public static ResolvedJavaField getCallVariantWrapperField(MetaAccessProvider metaAccess, CallVariant variant, boolean nonVirtual) { StringBuilder name = new StringBuilder(32); @@ -108,6 +113,11 @@ public JNIAccessibleMethod(JNIAccessibleClass declaringClass, int modifiers) { this.modifiers = modifiers; } + @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) + public boolean isNegative() { + return (modifiers & RuntimeMetadataDecoderImpl.NEGATIVE_FLAG_MASK) != 0; + } + @AlwaysInline("Work around an issue with the LLVM backend with which the return value was accessed incorrectly.") @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) CodePointer getCallWrapperAddress() { diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jni/access/JNIAccessibleMethodDescriptor.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jni/access/JNIAccessibleMethodDescriptor.java index 4926ce12f53f..2bb1fe5a58a7 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jni/access/JNIAccessibleMethodDescriptor.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jni/access/JNIAccessibleMethodDescriptor.java @@ -52,10 +52,6 @@ public static JNIAccessibleMethodDescriptor of(JavaMethod method) { } public static JNIAccessibleMethodDescriptor of(Executable method) { - StringBuilder sb = new StringBuilder("("); - for (Class type : method.getParameterTypes()) { - sb.append(MetaUtil.toInternalName(type.getName())); - } String name = method.getName(); Class returnType; if (method instanceof Constructor) { @@ -66,9 +62,24 @@ public static JNIAccessibleMethodDescriptor of(Executable method) { } else { throw VMError.shouldNotReachHereUnexpectedInput(method); // ExcludeFromJacocoGeneratedReport } - sb.append(')').append(MetaUtil.toInternalName(returnType.getName())); + return of(name, method.getParameterTypes(), returnType); + } + + public static JNIAccessibleMethodDescriptor of(String methodName, Class[] parameterTypes) { + return of(methodName, parameterTypes, null); + } + + private static JNIAccessibleMethodDescriptor of(String methodName, Class[] parameterTypes, Class returnType) { + StringBuilder sb = new StringBuilder("("); + for (Class type : parameterTypes) { + sb.append(MetaUtil.toInternalName(type.getName())); + } + sb.append(')'); + if (returnType != null) { + sb.append(MetaUtil.toInternalName(returnType.getName())); + } assert sb.indexOf(".") == -1 : "Malformed signature (needs to use '/' as package separator)"; - return new JNIAccessibleMethodDescriptor(name, sb.toString()); + return new JNIAccessibleMethodDescriptor(methodName, sb.toString()); } private final CharSequence name; @@ -95,6 +106,23 @@ public String getSignature() { return (String) signature; } + /** + * Performs a potentially costly conversion to string, only for slow paths. + */ + public String getNameConvertToString() { + return name.toString(); + } + + public String getSignatureWithoutReturnType() { + String signatureString = signature.toString(); + int parametersEnd = signatureString.lastIndexOf(')'); + if (!signatureString.isEmpty() && signatureString.charAt(0) == '(' && parametersEnd != -1) { + return signatureString.substring(0, parametersEnd + 1); + } else { + return null; + } + } + @Override public boolean equals(Object obj) { if (obj instanceof JNIAccessibleMethodDescriptor) { diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jni/access/JNIReflectionDictionary.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jni/access/JNIReflectionDictionary.java index f0bd475315a8..822436630278 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jni/access/JNIReflectionDictionary.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jni/access/JNIReflectionDictionary.java @@ -24,6 +24,7 @@ */ package com.oracle.svm.core.jni.access; +import static com.oracle.svm.core.MissingRegistrationUtils.throwMissingRegistrationErrors; import static com.oracle.svm.core.SubstrateOptions.JNIVerboseLookupErrors; import java.io.PrintStream; @@ -34,7 +35,6 @@ import org.graalvm.collections.Equivalence; import org.graalvm.collections.MapCursor; import org.graalvm.collections.UnmodifiableMapCursor; -import jdk.graal.compiler.word.Word; import org.graalvm.nativeimage.CurrentIsolate; import org.graalvm.nativeimage.ImageSingletons; import org.graalvm.nativeimage.Platform.HOSTED_ONLY; @@ -47,13 +47,18 @@ import com.oracle.svm.core.Isolates; import com.oracle.svm.core.SubstrateOptions; import com.oracle.svm.core.Uninterruptible; +import com.oracle.svm.core.jni.MissingJNIRegistrationUtils; import com.oracle.svm.core.jni.headers.JNIFieldId; import com.oracle.svm.core.jni.headers.JNIMethodId; import com.oracle.svm.core.log.Log; import com.oracle.svm.core.util.ImageHeapMap; import com.oracle.svm.core.util.Utf8.WrappedAsciiCString; +import com.oracle.svm.core.util.VMError; +import jdk.graal.compiler.util.SignatureUtil; +import jdk.graal.compiler.word.Word; import jdk.vm.ci.meta.JavaType; +import jdk.vm.ci.meta.MetaUtil; import jdk.vm.ci.meta.Signature; /** @@ -83,6 +88,8 @@ public int hashCode(Object o) { } }; + private static final JNIAccessibleClass NEGATIVE_CLASS_LOOKUP = new JNIAccessibleClass(); + public static void create() { ImageSingletons.add(JNIReflectionDictionary.class, new JNIReflectionDictionary()); } @@ -147,6 +154,12 @@ public JNIAccessibleClass addClassIfAbsent(Class classObj, Function, return classesByClassObject.get(classObj); } + public void addNegativeClassLookupIfAbsent(String typeName) { + String internalName = MetaUtil.toInternalName(typeName); + String queryName = internalName.startsWith("L") ? internalName.substring(1, internalName.length() - 1) : internalName; + classesByName.putIfAbsent(queryName, NEGATIVE_CLASS_LOOKUP); + } + @Platforms(HOSTED_ONLY.class) public void addLinkages(Map linkages) { nativeLinkages.putAll(EconomicMap.wrapMap(linkages)); @@ -158,10 +171,20 @@ public Iterable getClasses() { public Class getClassObjectByName(CharSequence name) { JNIAccessibleClass clazz = classesByName.get(name); + clazz = checkClass(clazz, name); dump(clazz == null, "getClassObjectByName"); return (clazz != null) ? clazz.getClassObject() : null; } + private static JNIAccessibleClass checkClass(JNIAccessibleClass clazz, CharSequence name) { + if (throwMissingRegistrationErrors() && clazz == null) { + MissingJNIRegistrationUtils.forClass(name.toString()); + } else if (clazz != null && clazz.isNegative()) { + return null; + } + return clazz; + } + /** * Gets the linkage for a native method. * @@ -232,6 +255,7 @@ private JNIAccessibleMethod getDeclaredMethod(Class classObject, JNIAccessibl public JNIMethodId getMethodID(Class classObject, CharSequence name, CharSequence signature, boolean isStatic) { JNIAccessibleMethod method = findMethod(classObject, new JNIAccessibleMethodDescriptor(name, signature), "getMethodID"); + method = checkMethod(method, classObject, name, signature); boolean match = (method != null && method.isStatic() == isStatic && method.isDiscoverableIn(classObject)); return toMethodID(match ? method : null); } @@ -253,7 +277,22 @@ public static JNIAccessibleMethod getMethodByID(JNIMethodId method) { if (SubstrateOptions.SpawnIsolates.getValue()) { p = p.add((UnsignedWord) Isolates.getHeapBase(CurrentIsolate.getIsolate())); } - return p.toObject(JNIAccessibleMethod.class, false); + JNIAccessibleMethod jniMethod = p.toObject(JNIAccessibleMethod.class, false); + VMError.guarantee(jniMethod == null || !jniMethod.isNegative(), "Existing methods can't correspond to a negative query"); + return jniMethod; + } + + private static JNIAccessibleMethod checkMethod(JNIAccessibleMethod method, Class clazz, CharSequence name, CharSequence signature) { + if (throwMissingRegistrationErrors() && method == null && SignatureUtil.isSignatureValid(signature.toString(), false)) { + /* + * A malformed signature never throws a missing registration error since it can't + * possibly match an existing method. + */ + MissingJNIRegistrationUtils.forMethod(clazz, name.toString(), signature.toString()); + } else if (method != null && method.isNegative()) { + return null; + } + return method; } private JNIAccessibleField getDeclaredField(Class classObject, CharSequence name, boolean isStatic, String dumpLabel) { @@ -261,7 +300,7 @@ private JNIAccessibleField getDeclaredField(Class classObject, CharSequence n dump(clazz == null && dumpLabel != null, dumpLabel); if (clazz != null) { JNIAccessibleField field = clazz.getField(name); - if (field != null && field.isStatic() == isStatic) { + if (field != null && (field.isStatic() == isStatic || field.isNegative())) { return field; } } @@ -270,6 +309,7 @@ private JNIAccessibleField getDeclaredField(Class classObject, CharSequence n public JNIFieldId getDeclaredFieldID(Class classObject, String name, boolean isStatic) { JNIAccessibleField field = getDeclaredField(classObject, name, isStatic, "getDeclaredFieldID"); + field = checkField(field, classObject, name); return (field != null) ? field.getId() : WordFactory.nullPointer(); } @@ -300,6 +340,7 @@ private JNIAccessibleField findSuperinterfaceField(Class clazz, CharSequence public JNIFieldId getFieldID(Class clazz, CharSequence name, boolean isStatic) { JNIAccessibleField field = findField(clazz, name, isStatic, "getFieldID"); + field = checkField(field, clazz, name); return (field != null && field.isDiscoverableIn(clazz)) ? field.getId() : WordFactory.nullPointer(); } @@ -310,6 +351,7 @@ public String getFieldNameByID(Class classObject, JNIFieldId id) { while (fieldsCursor.advance()) { JNIAccessibleField field = fieldsCursor.getValue(); if (id.equal(field.getId())) { + VMError.guarantee(!field.isNegative(), "Existing fields can't correspond to a negative query"); return (String) fieldsCursor.getKey(); } } @@ -317,6 +359,15 @@ public String getFieldNameByID(Class classObject, JNIFieldId id) { return null; } + private static JNIAccessibleField checkField(JNIAccessibleField field, Class clazz, CharSequence name) { + if (throwMissingRegistrationErrors() && field == null) { + MissingJNIRegistrationUtils.forField(clazz, name.toString()); + } else if (field != null && field.isNegative()) { + return null; + } + return field; + } + public static JNIAccessibleMethodDescriptor getMethodDescriptor(JNIAccessibleMethod method) { if (method != null) { JNIAccessibleClass clazz = method.getDeclaringClass(); diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/config/ReflectionRegistryAdapter.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/config/ReflectionRegistryAdapter.java index f29eb05d5f5d..0057ba44a92a 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/config/ReflectionRegistryAdapter.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/config/ReflectionRegistryAdapter.java @@ -24,11 +24,8 @@ */ package com.oracle.svm.hosted.config; -import static com.oracle.svm.core.MissingRegistrationUtils.throwMissingRegistrationErrors; - import java.lang.reflect.Proxy; import java.util.Arrays; -import java.util.List; import org.graalvm.nativeimage.impl.ConfigurationCondition; import org.graalvm.nativeimage.impl.RuntimeReflectionSupport; @@ -64,8 +61,6 @@ public TypeResult> resolveType(ConfigurationCondition condition, Config Throwable classLookupException = result.getException(); if (classLookupException instanceof LinkageError) { reflectionSupport.registerClassLookupException(condition, namedDescriptor.name(), classLookupException); - } else if (throwMissingRegistrationErrors() && classLookupException instanceof ClassNotFoundException) { - reflectionSupport.registerClassLookup(condition, namedDescriptor.name()); } } return result; @@ -130,43 +125,4 @@ public void registerPublicConstructors(ConfigurationCondition condition, boolean public void registerDeclaredConstructors(ConfigurationCondition condition, boolean queriedOnly, Class type) { reflectionSupport.registerAllDeclaredConstructorsQuery(condition, queriedOnly, type); } - - @Override - public void registerField(ConfigurationCondition condition, Class type, String fieldName, boolean allowWrite) throws NoSuchFieldException { - try { - super.registerField(condition, type, fieldName, allowWrite); - } catch (NoSuchFieldException e) { - if (throwMissingRegistrationErrors()) { - reflectionSupport.registerFieldLookup(condition, type, fieldName); - } else { - throw e; - } - } - } - - @Override - public void registerMethod(ConfigurationCondition condition, boolean queriedOnly, Class type, String methodName, List> methodParameterTypes) throws NoSuchMethodException { - try { - super.registerMethod(condition, queriedOnly, type, methodName, methodParameterTypes); - } catch (NoSuchMethodException e) { - if (throwMissingRegistrationErrors()) { - reflectionSupport.registerMethodLookup(condition, type, methodName, getParameterTypes(methodParameterTypes)); - } else { - throw e; - } - } - } - - @Override - public void registerConstructor(ConfigurationCondition condition, boolean queriedOnly, Class type, List> methodParameterTypes) throws NoSuchMethodException { - try { - super.registerConstructor(condition, queriedOnly, type, methodParameterTypes); - } catch (NoSuchMethodException e) { - if (throwMissingRegistrationErrors()) { - reflectionSupport.registerConstructorLookup(condition, type, getParameterTypes(methodParameterTypes)); - } else { - throw e; - } - } - } } diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/config/RegistryAdapter.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/config/RegistryAdapter.java index d8c484c14143..0bf827a570cd 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/config/RegistryAdapter.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/config/RegistryAdapter.java @@ -24,6 +24,8 @@ */ package com.oracle.svm.hosted.config; +import static com.oracle.svm.core.MissingRegistrationUtils.throwMissingRegistrationErrors; + import java.lang.reflect.Executable; import java.lang.reflect.Method; import java.lang.reflect.Modifier; @@ -73,7 +75,14 @@ public void registerType(ConfigurationCondition condition, Class type) { public TypeResult> resolveType(ConfigurationCondition condition, ConfigurationTypeDescriptor typeDescriptor, boolean allowPrimitives, boolean includeAllElements) { switch (typeDescriptor.getDescriptorType()) { case NAMED -> { - return resolveNamedType(((NamedConfigurationTypeDescriptor) typeDescriptor), allowPrimitives); + NamedConfigurationTypeDescriptor namedDescriptor = (NamedConfigurationTypeDescriptor) typeDescriptor; + TypeResult> result = resolveNamedType(namedDescriptor, allowPrimitives); + if (!result.isPresent()) { + if (throwMissingRegistrationErrors() && result.getException() instanceof ClassNotFoundException) { + registry.registerClassLookup(condition, namedDescriptor.name()); + } + } + return result; } case PROXY -> { return resolveProxyType((ProxyConfigurationTypeDescriptor) typeDescriptor); @@ -85,7 +94,22 @@ public TypeResult> resolveType(ConfigurationCondition condition, Config } private TypeResult> resolveNamedType(NamedConfigurationTypeDescriptor typeDescriptor, boolean allowPrimitives) { - return classLoader.findClass(typeDescriptor.name(), allowPrimitives); + TypeResult> result = classLoader.findClass(typeDescriptor.name(), allowPrimitives); + if (!result.isPresent() && result.getException() instanceof NoClassDefFoundError) { + /* + * In certain cases when the class name is identical to an existing class name except + * for lettercase, `ClassLoader.findClass` throws a `NoClassDefFoundError` but + * `Class.forName` throws a `ClassNotFoundException`. + */ + try { + Class.forName(typeDescriptor.name()); + } catch (ClassNotFoundException notFoundException) { + result = TypeResult.forException(typeDescriptor.name(), notFoundException); + } catch (Throwable t) { + // ignore + } + } + return result; } private TypeResult> resolveProxyType(ProxyConfigurationTypeDescriptor typeDescriptor) { @@ -165,7 +189,15 @@ public void registerDeclaredConstructors(ConfigurationCondition condition, boole @Override public void registerField(ConfigurationCondition condition, Class type, String fieldName, boolean allowWrite) throws NoSuchFieldException { - registry.register(condition, allowWrite, type.getDeclaredField(fieldName)); + try { + registry.register(condition, allowWrite, type.getDeclaredField(fieldName)); + } catch (NoSuchFieldException e) { + if (throwMissingRegistrationErrors()) { + registry.registerFieldLookup(condition, type, fieldName); + } else { + throw e; + } + } } @Override @@ -201,32 +233,49 @@ public void registerUnsafeAllocated(ConfigurationCondition condition, Class c @Override public void registerMethod(ConfigurationCondition condition, boolean queriedOnly, Class type, String methodName, List> methodParameterTypes) throws NoSuchMethodException { - Class[] parameterTypesArray = getParameterTypes(methodParameterTypes); - Method method; try { - method = type.getDeclaredMethod(methodName, parameterTypesArray); - } catch (NoClassDefFoundError e) { - /* - * getDeclaredMethod() builds a set of all the declared methods, which can fail when a - * symbolic reference from another method to a type (via parameters, return value) - * cannot be resolved. getMethod() builds a different set of methods and can still - * succeed. This case must be handled for predefined classes when, during the run - * observed by the agent, a referenced class was not loaded and is not available now - * precisely because the application used getMethod() instead of getDeclaredMethod(). - */ + Class[] parameterTypesArray = getParameterTypes(methodParameterTypes); + Method method; try { - method = type.getMethod(methodName, parameterTypesArray); - } catch (Throwable ignored) { + method = type.getDeclaredMethod(methodName, parameterTypesArray); + } catch (NoClassDefFoundError e) { + /* + * getDeclaredMethod() builds a set of all the declared methods, which can fail when + * a symbolic reference from another method to a type (via parameters, return value) + * cannot be resolved. getMethod() builds a different set of methods and can still + * succeed. This case must be handled for predefined classes when, during the run + * observed by the agent, a referenced class was not loaded and is not available now + * precisely because the application used getMethod() instead of + * getDeclaredMethod(). + */ + try { + method = type.getMethod(methodName, parameterTypesArray); + } catch (Throwable ignored) { + throw e; + } + } + registerExecutable(condition, queriedOnly, method); + } catch (NoSuchMethodException e) { + if (throwMissingRegistrationErrors()) { + registry.registerMethodLookup(condition, type, methodName, getParameterTypes(methodParameterTypes)); + } else { throw e; } } - registerExecutable(condition, queriedOnly, method); } @Override public void registerConstructor(ConfigurationCondition condition, boolean queriedOnly, Class type, List> methodParameterTypes) throws NoSuchMethodException { Class[] parameterTypesArray = getParameterTypes(methodParameterTypes); - registerExecutable(condition, queriedOnly, type.getDeclaredConstructor(parameterTypesArray)); + try { + registerExecutable(condition, queriedOnly, type.getDeclaredConstructor(parameterTypesArray)); + } catch (NoSuchMethodException e) { + if (throwMissingRegistrationErrors()) { + registry.registerConstructorLookup(condition, type, getParameterTypes(methodParameterTypes)); + } else { + throw e; + } + } } static Class[] getParameterTypes(List> methodParameterTypes) { diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/jni/JNIAccessFeature.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/jni/JNIAccessFeature.java index 48c2b3d403dd..3a98a9a8ca38 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/jni/JNIAccessFeature.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/jni/JNIAccessFeature.java @@ -31,6 +31,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.HashSet; import java.util.IdentityHashMap; import java.util.List; import java.util.Map; @@ -42,6 +43,7 @@ import org.graalvm.collections.EconomicSet; import org.graalvm.collections.Equivalence; +import org.graalvm.collections.Pair; import org.graalvm.collections.UnmodifiableMapCursor; import org.graalvm.nativeimage.ImageSingletons; import org.graalvm.nativeimage.c.function.CodePointer; @@ -168,8 +170,11 @@ static final class JNICallableJavaMethod { private int loadedConfigurations; private final Set> newClasses = Collections.newSetFromMap(new ConcurrentHashMap<>()); + private final Set newNegativeClassLookups = Collections.newSetFromMap(new ConcurrentHashMap<>()); private final Set newMethods = Collections.newSetFromMap(new ConcurrentHashMap<>()); + private final Map, Set[]>>> newNegativeMethodLookups = new ConcurrentHashMap<>(); private final Map newFields = new ConcurrentHashMap<>(); + private final Map, Set> newNegativeFieldLookups = new ConcurrentHashMap<>(); private final Map newLinkages = new ConcurrentHashMap<>(); private final Map nativeLinkages = new ConcurrentHashMap<>(); @@ -237,6 +242,41 @@ private void registerFields(boolean finalIsWritable, Field[] fields) { } } + @Override + public void registerClassLookup(ConfigurationCondition condition, String typeName) { + try { + register(condition, false, Class.forName(typeName)); + } catch (ClassNotFoundException e) { + newNegativeClassLookups.add(typeName); + } + } + + @Override + public void registerFieldLookup(ConfigurationCondition condition, Class declaringClass, String fieldName) { + try { + register(condition, false, declaringClass.getDeclaredField(fieldName)); + } catch (NoSuchFieldException e) { + newNegativeFieldLookups.computeIfAbsent(declaringClass, (clazz) -> new HashSet<>()).add(fieldName); + } + } + + @Override + public void registerMethodLookup(ConfigurationCondition condition, Class declaringClass, String methodName, Class... parameterTypes) { + try { + register(condition, false, declaringClass.getDeclaredMethod(methodName, parameterTypes)); + } catch (NoSuchMethodException e) { + newNegativeMethodLookups.computeIfAbsent(declaringClass, (clazz) -> new HashSet<>()).add(Pair.create(methodName, parameterTypes)); + } + } + + @Override + public void registerConstructorLookup(ConfigurationCondition condition, Class declaringClass, Class... parameterTypes) { + try { + register(condition, false, declaringClass.getDeclaredConstructor(parameterTypes)); + } catch (NoSuchMethodException e) { + newNegativeMethodLookups.computeIfAbsent(declaringClass, (clazz) -> new HashSet<>()).add(Pair.create("", parameterTypes)); + } + } } @Override @@ -312,7 +352,8 @@ public JNINativeLinkage makeLinkage(String declaringClass, String name, String d } private boolean wereElementsAdded() { - return !(newClasses.isEmpty() && newMethods.isEmpty() && newFields.isEmpty() && newLinkages.isEmpty()); + return !(newClasses.isEmpty() && newMethods.isEmpty() && newFields.isEmpty() && newLinkages.isEmpty() && + newNegativeClassLookups.isEmpty() && newNegativeFieldLookups.isEmpty() && newNegativeMethodLookups.isEmpty()); } @Override @@ -328,16 +369,35 @@ public void duringAnalysis(DuringAnalysisAccess a) { } newClasses.clear(); + for (String className : newNegativeClassLookups) { + addNegativeClassLookup(className); + } + newNegativeClassLookups.clear(); + for (Executable method : newMethods) { addMethod(method, access); } newMethods.clear(); + newNegativeMethodLookups.forEach((clazz, signatures) -> { + for (Pair[]> signature : signatures) { + addNegativeMethodLookup(clazz, signature.getLeft(), signature.getRight(), access); + } + }); + newNegativeMethodLookups.clear(); + newFields.forEach((field, writable) -> { addField(field, writable, access); }); newFields.clear(); + newNegativeFieldLookups.forEach((clazz, fieldNames) -> { + for (String fieldName : fieldNames) { + addNegativeFieldLookup(clazz, fieldName, access); + } + }); + newNegativeFieldLookups.clear(); + JNIReflectionDictionary.singleton().addLinkages(newLinkages); newLinkages.clear(); @@ -362,6 +422,10 @@ private static JNIAccessibleClass addClass(Class classObj, DuringAnalysisAcce }); } + private static void addNegativeClassLookup(String className) { + JNIReflectionDictionary.singleton().addNegativeClassLookupIfAbsent(className); + } + private void addMethod(Executable method, DuringAnalysisAccessImpl access) { if (SubstitutionReflectivityFilter.shouldExclude(method, access.getMetaAccess(), access.getUniverse())) { return; @@ -407,6 +471,12 @@ private void addMethod(Executable method, DuringAnalysisAccessImpl access) { }); } + private static void addNegativeMethodLookup(Class declaringClass, String methodName, Class[] parameterTypes, DuringAnalysisAccessImpl access) { + JNIAccessibleClass jniClass = addClass(declaringClass, access); + JNIAccessibleMethodDescriptor descriptor = JNIAccessibleMethodDescriptor.of(methodName, parameterTypes); + jniClass.addMethodIfAbsent(descriptor, d -> JNIAccessibleMethod.negativeMethodQuery(jniClass)); + } + private JNIJavaCallVariantWrapperGroup createJavaCallVariantWrappers(DuringAnalysisAccessImpl access, ResolvedSignature wrapperSignature, boolean nonVirtual) { var map = nonVirtual ? nonvirtualCallVariantWrappers : callVariantWrappers; return map.computeIfAbsent(wrapperSignature, signature -> { @@ -452,6 +522,11 @@ private static void addField(Field reflField, boolean writable, DuringAnalysisAc bb.registerAsJNIAccessed(field, writable); } + private static void addNegativeFieldLookup(Class declaringClass, String fieldName, DuringAnalysisAccessImpl access) { + JNIAccessibleClass jniClass = addClass(declaringClass, access); + jniClass.addFieldIfAbsent(fieldName, d -> JNIAccessibleField.negativeFieldQuery(jniClass)); + } + @Override @SuppressWarnings("unused") public void afterAnalysis(AfterAnalysisAccess access) { @@ -467,11 +542,15 @@ public void afterAnalysis(AfterAnalysisAccess access) { numClasses++; var fieldsCursor = clazz.getFields(); while (fieldsCursor.advance()) { - numFields++; + if (!fieldsCursor.getValue().isNegativeHosted()) { + numFields++; + } } var methodsCursor = clazz.getMethods(); while (methodsCursor.advance()) { - numMethods++; + if (!methodsCursor.getValue().isNegative()) { + numMethods++; + } } } ProgressReporter.singleton().setJNIInfo(numClasses, numFields, numMethods); @@ -617,23 +696,26 @@ private static EconomicSet> findHidingSubclasses0(HostedType type, Pred private static void finishFieldBeforeCompilation(String name, JNIAccessibleField field, CompilationAccessImpl access, DynamicHubLayout dynamicHubLayout) { try { - Class declaringClass = field.getDeclaringClass().getClassObject(); - Field reflField = declaringClass.getDeclaredField(name); - HostedField hField = access.getMetaAccess().lookupJavaField(reflField); - int offset; - if (dynamicHubLayout.isInlinedField(hField)) { - throw VMError.shouldNotReachHere("DynamicHub inlined fields are not accessible %s", hField); - } else if (HybridLayout.isHybridField(hField)) { - assert !hField.hasLocation(); - HybridLayout hybridLayout = new HybridLayout((HostedInstanceClass) hField.getDeclaringClass(), - ImageSingletons.lookup(ObjectLayout.class), access.getMetaAccess()); - assert hField.equals(hybridLayout.getArrayField()) : "JNI access to hybrid objects is implemented only for the array field"; - offset = hybridLayout.getArrayBaseOffset(); - } else { - assert hField.hasLocation(); - offset = hField.getLocation(); + int offset = -1; + EconomicSet> hidingSubclasses = null; + if (!field.isNegativeHosted()) { + Class declaringClass = field.getDeclaringClass().getClassObject(); + Field reflField = declaringClass.getDeclaredField(name); + HostedField hField = access.getMetaAccess().lookupJavaField(reflField); + if (dynamicHubLayout.isInlinedField(hField)) { + throw VMError.shouldNotReachHere("DynamicHub inlined fields are not accessible %s", hField); + } else if (HybridLayout.isHybridField(hField)) { + assert !hField.hasLocation(); + HybridLayout hybridLayout = new HybridLayout((HostedInstanceClass) hField.getDeclaringClass(), + ImageSingletons.lookup(ObjectLayout.class), access.getMetaAccess()); + assert hField.equals(hybridLayout.getArrayField()) : "JNI access to hybrid objects is implemented only for the array field"; + offset = hybridLayout.getArrayBaseOffset(); + } else { + assert hField.hasLocation(); + offset = hField.getLocation(); + } + hidingSubclasses = findHidingSubclasses(hField.getDeclaringClass(), sub -> anyFieldMatches(sub, name)); } - EconomicSet> hidingSubclasses = findHidingSubclasses(hField.getDeclaringClass(), sub -> anyFieldMatches(sub, name)); field.finishBeforeCompilation(offset, hidingSubclasses);