Skip to content

Commit

Permalink
Throw exceptions for missing resource bundle registrations
Browse files Browse the repository at this point in the history
  • Loading branch information
loicottet committed Jan 9, 2024
1 parent 6df04c9 commit e92bb4d
Show file tree
Hide file tree
Showing 11 changed files with 213 additions and 72 deletions.
1 change: 1 addition & 0 deletions substratevm/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ This changelog summarizes major changes to GraalVM Native Image.
* (GR-30433) Disallow the deprecated environment variable USE_NATIVE_IMAGE_JAVA_PLATFORM_MODULE_SYSTEM=false.
* (GR-49655) Experimental support for parts of the [Foreign Function & Memory API](https://github.com/oracle/graal/blob/master/docs/reference-manual/native-image/ForeignInterface.md) (part of "Project Panama", [JEP 454](https://openjdk.org/jeps/454)) on AMD64. Must be enabled with `-H:+ForeignAPISupport` (requiring `-H:+UnlockExperimentalVMOptions`).
* (GR-46407) Correctly rethrow build-time linkage errors at run-time for registered reflection queries.
* (GR-50529) Native Image now throws a specific error when trying to access unregistered resource bundles instead of failing on a subsequent reflection or resource query.

## 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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -687,33 +687,48 @@ private static boolean getBundleImpl(JNIEnvironment jni, JNIObjectHandle thread,
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) {
private static boolean loadBundleOf(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();
}
JNIObjectHandle baseName = getObjectArgument(thread, 0);
JNIObjectHandle locale = getObjectArgument(thread, 1);
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;
}

private static String readLocaleTag(JNIEnvironment jni, JNIObjectHandle locale) {
JNIObjectHandle languageTag = Support.callObjectMethod(jni, locale, agent.handles().getJavaUtilLocaleToLanguageTag(jni));
JNIObjectHandle languageTag = Support.callObjectMethod(jni, locale, agent.handles().javaUtilLocaleToLanguageTag);
if (clearException(jni)) {
/*- return root locale */
return "";
}
return fromJniString(jni, languageTag);

JNIObjectHandle reconstructedLocale = Support.callStaticObjectMethodL(jni, agent.handles().javaUtilLocale, agent.handles().javaUtilLocaleForLanguageTag, languageTag);
if (clearException(jni)) {
reconstructedLocale = nullHandle();
}
if (Support.callBooleanMethodL(jni, locale, agent.handles().javaUtilLocaleEquals, reconstructedLocale)) {
return fromJniString(jni, languageTag);
} else {
/*
* Ill-formed locale, we do our best to return a unique description of the locale.
* Locale.toLanguageTag simply ignores ill-formed locale elements, which creates
* confusion when trying to determine whether a specific locale was registered.
*/
String language = getElementString(jni, locale, agent.handles().javaUtilLocaleGetLanguage);
String country = getElementString(jni, locale, agent.handles().javaUtilLocaleGetCountry);
String variant = getElementString(jni, locale, agent.handles().javaUtilLocaleGetVariant);

return String.join("-", language, country, variant);
}
}

private static String getElementString(JNIEnvironment jni, JNIObjectHandle object, JNIMethodId getter) {
JNIObjectHandle valueHandle = Support.callObjectMethod(jni, object, getter);
if (clearException(jni)) {
valueHandle = nullHandle();
}
return valueHandle.notEqual(nullHandle()) ? fromJniString(jni, valueHandle) : "";
}

private static boolean loadClass(JNIEnvironment jni, JNIObjectHandle thread, Breakpoint bp, InterceptedState state) {
Expand Down Expand Up @@ -1553,8 +1568,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),
brk("sun/util/resources/Bundles", "loadBundleOf", "(Ljava/lang/String;Ljava/util/Locale;Lsun/util/resources/Bundles$Strategy;)Ljava/util/ResourceBundle;",
BreakpointInterceptor::loadBundleOf),

// 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),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,15 +80,16 @@ public class NativeImageAgentJNIHandleSet extends JNIHandleSet {
private JNIObjectHandle javaLangReflectProxy = WordFactory.nullPointer();
private JNIMethodId javaLangReflectProxyIsProxyClass = WordFactory.nullPointer();

private JNIMethodId javaUtilLocaleToLanguageTag;
private JNIFieldId javaUtilResourceBundleParentField;
private JNIMethodId javaUtilResourceBundleGetLocale;
final JNIObjectHandle javaUtilLocale;
final JNIMethodId javaUtilLocaleGetLanguage;
final JNIMethodId javaUtilLocaleGetCountry;
final JNIMethodId javaUtilLocaleGetVariant;
final JNIMethodId javaUtilLocaleForLanguageTag;
final JNIMethodId javaUtilLocaleToLanguageTag;
final JNIMethodId javaUtilLocaleEquals;

final JNIFieldId javaLangInvokeSerializedLambdaCapturingClass;

final JNIMethodId sunUtilResourcesBundlesCacheKeyGetName;
final JNIMethodId sunUtilResourcesBundlesCacheKeyGetLocale;

final JNIMethodId javaLangModuleGetName;

NativeImageAgentJNIHandleSet(JNIEnvironment env) {
Expand Down Expand Up @@ -128,12 +129,16 @@ 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);

javaUtilLocale = newClassGlobalRef(env, "java/util/Locale");
javaUtilLocaleGetLanguage = getMethodId(env, javaUtilLocale, "getLanguage", "()Ljava/lang/String;", false);
javaUtilLocaleGetCountry = getMethodId(env, javaUtilLocale, "getCountry", "()Ljava/lang/String;", false);
javaUtilLocaleGetVariant = getMethodId(env, javaUtilLocale, "getVariant", "()Ljava/lang/String;", false);
javaUtilLocaleForLanguageTag = getMethodId(env, javaUtilLocale, "forLanguageTag", "(Ljava/lang/String;)Ljava/util/Locale;", true);
javaUtilLocaleEquals = getMethodId(env, javaUtilLocale, "equals", "(Ljava/lang/Object;)Z", false);
javaUtilLocaleToLanguageTag = getMethodId(env, javaUtilLocale, "toLanguageTag", "()Ljava/lang/String;", false);
}

JNIMethodId getJavaLangReflectExecutableGetParameterTypes(JNIEnvironment env) {
Expand Down Expand Up @@ -239,28 +244,4 @@ JNIMethodId getJavaLangReflectProxyIsProxyClass(JNIEnvironment env) {
}
return javaLangReflectProxyIsProxyClass;
}

public JNIMethodId getJavaUtilLocaleToLanguageTag(JNIEnvironment env) {
if (javaUtilLocaleToLanguageTag.isNull()) {
JNIObjectHandle javaUtilLocale = findClass(env, "java/util/Locale");
javaUtilLocaleToLanguageTag = getMethodId(env, javaUtilLocale, "toLanguageTag", "()Ljava/lang/String;", false);
}
return javaUtilLocaleToLanguageTag;
}

public JNIFieldId getJavaUtilResourceBundleParentField(JNIEnvironment env) {
if (javaUtilResourceBundleParentField.isNull()) {
JNIObjectHandle javaUtilResourceBundle = findClass(env, "java/util/ResourceBundle");
javaUtilResourceBundleParentField = getFieldId(env, javaUtilResourceBundle, "parent", "Ljava/util/ResourceBundle;", false);
}
return javaUtilResourceBundleParentField;
}

public JNIMethodId getJavaUtilResourceBundleGetLocale(JNIEnvironment env) {
if (javaUtilResourceBundleGetLocale.isNull()) {
JNIObjectHandle javaUtilResourceBundle = findClass(env, "java/util/ResourceBundle");
javaUtilResourceBundleGetLocale = getMethodId(env, javaUtilResourceBundle, "getLocale", "()Ljava/util/Locale;", false);
}
return javaUtilResourceBundleGetLocale;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ public final class AccessAdvisor {
internalCallerFilter.addOrGetChildren("java.util.**", ConfigurationFilter.Inclusion.Exclude);
internalCallerFilter.addOrGetChildren("java.util.concurrent.atomic.*", ConfigurationFilter.Inclusion.Include); // Atomic*FieldUpdater
internalCallerFilter.addOrGetChildren("java.util.Collections", ConfigurationFilter.Inclusion.Include); // java.util.Collections.zeroLengthArray
// LogRecord.readObject looks up resource bundles
internalCallerFilter.addOrGetChildren("java.util.logging.LogRecord", ConfigurationFilter.Inclusion.Include);
internalCallerFilter.addOrGetChildren("java.util.random.*", ConfigurationFilter.Inclusion.Include); // RandomGeneratorFactory$$Lambda
/*
* ForkJoinTask.getThrowableException calls Class.getConstructors and
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,7 @@ public void processEntry(EconomicMap<String, ?> entry, ConfigurationSet configur
break;
}

case "putBundleInCache":
case "loadBundleOf":
case "getBundleImpl": {
expectSize(args, 5);
String baseName = (String) args.get(2);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,16 @@

import static com.oracle.svm.util.StringUtil.toDotSeparated;
import static com.oracle.svm.util.StringUtil.toSlashSeparated;
import static sun.util.locale.provider.LocaleProviderAdapter.Type.CLDR;

import java.lang.reflect.Constructor;
import java.nio.charset.Charset;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.IllformedLocaleException;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
Expand All @@ -43,6 +46,7 @@
import java.util.function.Function;
import java.util.stream.Collectors;

import org.graalvm.collections.EconomicMap;
import org.graalvm.nativeimage.ImageSingletons;
import org.graalvm.nativeimage.Platform;
import org.graalvm.nativeimage.Platforms;
Expand All @@ -55,8 +59,11 @@
import com.oracle.svm.core.SubstrateUtil;
import com.oracle.svm.core.jdk.Resources;
import com.oracle.svm.core.util.VMError;
import com.oracle.svm.util.ReflectionUtil;

import jdk.graal.compiler.debug.GraalError;
import sun.util.locale.provider.LocaleProviderAdapter;
import sun.util.locale.provider.ResourceBundleBasedAdapter;
import sun.util.resources.Bundles;

/**
Expand All @@ -79,6 +86,8 @@ public class LocalizationSupport {

public final Charset defaultCharset;

private final EconomicMap<String, Set<Locale>> registeredBundles = EconomicMap.create();

public LocalizationSupport(Locale defaultLocale, Set<Locale> locales, Charset defaultCharset) {
this.defaultLocale = defaultLocale;
this.allLocales = locales.toArray(new Locale[0]);
Expand Down Expand Up @@ -186,10 +195,22 @@ public void registerRequiredReflectionAndResourcesForBundle(String baseName, Col
}
}

private void registerRequiredReflectionAndResourcesForBundleAndLocale(String baseName, Locale baseLocale) {
for (Locale locale : control.getCandidateLocales(baseName, baseLocale)) {
public void registerRequiredReflectionAndResourcesForBundleAndLocale(String baseName, Locale baseLocale) {
/*
* Bundles in the sun.(text|util).resources.cldr packages are loaded with an alternative
* strategy which tries parent aliases defined in CLDRBaseLocaleDataMetaInfo.parentLocales.
*/
List<Locale> candidateLocales = isCLDRBundle(baseName)
? ((ResourceBundleBasedAdapter) LocaleProviderAdapter.forType(CLDR)).getCandidateLocales(baseName, baseLocale)
: control.getCandidateLocales(baseName, baseLocale);

for (Locale locale : candidateLocales) {
String bundleWithLocale = control.toBundleName(baseName, locale);
RuntimeReflection.registerClassLookup(bundleWithLocale);
Class<?> bundleClass = ReflectionUtil.lookupClass(true, bundleWithLocale);
if (bundleClass != null) {
registerNullaryConstructor(bundleClass);
}
Resources.singleton().registerNegativeQuery(bundleWithLocale.replace('.', '/') + ".properties");
String otherBundleName = Bundles.toOtherBundleName(baseName, bundleWithLocale, locale);
if (!otherBundleName.equals(bundleWithLocale)) {
Expand All @@ -198,6 +219,10 @@ private void registerRequiredReflectionAndResourcesForBundleAndLocale(String bas
}
}

private boolean isCLDRBundle(String baseName) {
return baseName.startsWith(CLDR.getUtilResourcesPackage()) || baseName.startsWith(CLDR.getTextResourcesPackage());
}

/**
* Template method for subclasses to perform additional tasks.
*/
Expand Down Expand Up @@ -273,4 +298,26 @@ private void registerNullaryConstructor(Class<?> bundleClass) {
}
RuntimeReflection.register(nullaryConstructor);
}

@Platforms(Platform.HOSTED_ONLY.class)
public void registerBundleLookup(String baseName, Locale locale) {
registeredBundles.putIfAbsent(baseName, new HashSet<>());
registeredBundles.get(baseName).add(locale);
}

public boolean isRegisteredBundleLookup(String baseName, Locale locale, Object controlOrStrategy) {
if (baseName == null || locale == null || controlOrStrategy == null) {
/* Those cases will throw a NullPointerException before any lookup */
return true;
}
if (registeredBundles.get(baseName, Collections.emptySet()).contains(locale)) {
return true;
}
/*
* We cannot guarantee whether non-canonicalizable locales were present in the
* configuration, so we do not throw in that case. The exception thrown when handling the
* locale will be enough for debugging.
*/
return !Locale.forLanguageTag(locale.toLanguageTag()).equals(locale);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,10 @@
*/
package com.oracle.svm.core.jdk.localization.substitutions;

import static sun.security.util.SecurityConstants.GET_CLASSLOADER_PERMISSION;

import java.util.Locale;
import java.util.Objects;
import java.util.ResourceBundle;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
Expand All @@ -38,6 +41,9 @@
import com.oracle.svm.core.annotate.TargetElement;
import com.oracle.svm.core.jdk.localization.LocalizationSupport;
import com.oracle.svm.core.jdk.localization.substitutions.modes.OptimizedLocaleMode;
import com.oracle.svm.core.jdk.resources.MissingResourceRegistrationUtils;

import jdk.internal.loader.BootLoader;

@TargetClass(java.util.ResourceBundle.class)
@SuppressWarnings({"unused"})
Expand Down Expand Up @@ -98,4 +104,64 @@ private static ResourceBundle getBundle(String baseName, @SuppressWarnings("unus
private static ResourceBundle getBundle(String baseName, Locale targetLocale, @SuppressWarnings("unused") Module module) {
return ImageSingletons.lookup(LocalizationSupport.class).asOptimizedSupport().getCached(baseName, targetLocale);
}

@Substitute
private static ResourceBundle getBundleImpl(String baseName,
Locale locale,
Class<?> caller,
ClassLoader loader,
ResourceBundle.Control control) {
Module callerModule = getCallerModule(caller);

// get resource bundles for a named module only if loader is the module's class loader
if (callerModule.isNamed() && loader == getLoader(callerModule)) {
if (!ImageSingletons.lookup(LocalizationSupport.class).isRegisteredBundleLookup(baseName, locale, control)) {
MissingResourceRegistrationUtils.missingResourceBundle(baseName, locale);
}
return getBundleImpl(callerModule, callerModule, baseName, locale, control);
}

// find resource bundles from unnamed module of given class loader
// Java agent can add to the bootclasspath e.g. via
// java.lang.instrument.Instrumentation and load classes in unnamed module.
// It may call RB::getBundle that will end up here with loader == null.
Module unnamedModule = loader != null
? loader.getUnnamedModule()
: BootLoader.getUnnamedModule();

if (!ImageSingletons.lookup(LocalizationSupport.class).isRegisteredBundleLookup(baseName, locale, control)) {
MissingResourceRegistrationUtils.missingResourceBundle(baseName, locale);
}
return getBundleImpl(callerModule, unnamedModule, baseName, locale, control);
}

@Substitute
private static ResourceBundle getBundleFromModule(Class<?> caller,
Module module,
String baseName,
Locale locale,
ResourceBundle.Control control) {
Objects.requireNonNull(module);
Module callerModule = getCallerModule(caller);
if (callerModule != module) {
@SuppressWarnings("removal")
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkPermission(GET_CLASSLOADER_PERMISSION);
}
}
if (!ImageSingletons.lookup(LocalizationSupport.class).isRegisteredBundleLookup(baseName, locale, control)) {
MissingResourceRegistrationUtils.missingResourceBundle(baseName, locale);
}
return getBundleImpl(callerModule, module, baseName, locale, control);
}

@Alias
private static native Module getCallerModule(Class<?> caller);

@Alias
private static native ClassLoader getLoader(Module module);

@Alias
private static native ResourceBundle getBundleImpl(Module callerModule, Module module, String baseName, Locale locale, ResourceBundle.Control control);
}
Loading

0 comments on commit e92bb4d

Please sign in to comment.