diff --git a/docs/reference-manual/native-image/JCASecurityServices.md b/docs/reference-manual/native-image/JCASecurityServices.md index 1b8326c2b093..15e25c1df7b4 100644 --- a/docs/reference-manual/native-image/JCASecurityServices.md +++ b/docs/reference-manual/native-image/JCASecurityServices.md @@ -36,11 +36,20 @@ The report will detail all registered service classes, the API methods that trig > Note: The `--enable-all-security-services` option is now deprecated and it will be removed in a future release. +## Provider Initialization + +Currently security providers are initialized at build time. +To move their initialization to run time, use the option `--future-defaults=all` or `--future-defaults=run-time-initialized-jdk`. +Provider verification will still occur at build time. +Run-time initialization of security providers helps reduce image heap size. +To move their initialization to run time, you can use the flag `--future-defaults=all` or `--future-defaults=run-time-initialized-jdk`. + ## Provider Registration The `native-image` builder captures the list of providers and their preference order from the underlying JVM. The provider order is specified in the `java.security` file under `/conf/security/java.security`. -New security providers cannot be registered at run time; all providers must be statically configured at executable build time. +New security providers cannot be registered at run time by default (see the section above); all providers must be statically configured at executable build time. +If the user specifies `--future-defaults=all` or `--future-defaults=run-time-initialized-jdk` to move providers initialization to run time, then a specific properties file can be used via the command line option `-Djava.security.properties=`. ## Providers Reordering at Run Time @@ -52,6 +61,9 @@ Security.removeProvider("BC"); Security.insertProviderAt(bcProvider, 1); ``` +If `--future-defaults=all` or `--future-defaults=run-time-initialized-jdk` is enabled, the list of providers is constructed at run time. +The same approach to manipulating providers can then be used. + ## SecureRandom The `SecureRandom` implementations open the `/dev/random` and `/dev/urandom` files which are used as sources. diff --git a/substratevm/CHANGELOG.md b/substratevm/CHANGELOG.md index 2a6b71f92359..2028618b0f67 100644 --- a/substratevm/CHANGELOG.md +++ b/substratevm/CHANGELOG.md @@ -28,6 +28,9 @@ This changelog summarizes major changes to GraalVM Native Image. * (GR-47881) Remove the total number of loaded types, fields, and methods from the build output, deprecated these metrics in the build output schema, and removed already deprecated build output metrics. * (GR-64619) Missing registration errors are now subclasses of `LinkageError` * (GR-63591) Resource bundle registration is now included as part of the `"resources"` section of _reachability-metadata.json_. When this is the case, the bundle name is specified using the `"bundle"` field. +* (GR-57827) Move the initialization of security providers from build time to runtime. +* (GR-57827) Security providers can now be initialized at run time (instead of build time) when using the option `--future-defaults=all` or `--future-defaults=run-time-initialized-jdk`. +Run-time initialization of security providers helps reduce image heap size by avoiding unnecessary objects inclusion. ## GraalVM for JDK 24 (Internal Version 24.2.0) * (GR-59717) Added `DuringSetupAccess.registerObjectReachabilityHandler` to allow registering a callback that is executed when an object of a specified type is marked as reachable during heap scanning. diff --git a/substratevm/mx.substratevm/suite.py b/substratevm/mx.substratevm/suite.py index a4d9e94217cf..e866721d3e6f 100644 --- a/substratevm/mx.substratevm/suite.py +++ b/substratevm/mx.substratevm/suite.py @@ -352,6 +352,7 @@ ], "requiresConcealed" : { "java.base" : [ + "com.sun.crypto.provider", "sun.invoke.util", "sun.net", "sun.net.www", @@ -362,6 +363,8 @@ "sun.reflect.generics.repository", "sun.reflect.generics.tree", "sun.security.jca", + "sun.security.provider", + "sun.security.rsa", "sun.security.ssl", "sun.security.util", "sun.text.spi", diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/SecurityProvidersSupport.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/SecurityProvidersSupport.java new file mode 100644 index 000000000000..fd3d2ed11e05 --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/SecurityProvidersSupport.java @@ -0,0 +1,188 @@ +/* + * 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.jdk; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.security.Provider; +import java.util.List; +import java.util.Properties; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import org.graalvm.collections.EconomicMap; +import org.graalvm.collections.EconomicSet; +import org.graalvm.nativeimage.ImageSingletons; +import org.graalvm.nativeimage.Platform; +import org.graalvm.nativeimage.Platforms; + +import com.oracle.svm.core.util.ImageHeapMap; +import com.oracle.svm.core.util.VMError; + +import jdk.graal.compiler.api.replacements.Fold; +import sun.security.util.Debug; + +/** + * The class that holds various build-time and run-time structures necessary for security providers, + * but only in case they are initialized at run time (see the + * JCA Security Services documentation for details). + */ +public final class SecurityProvidersSupport { + /** + * A set of providers to be loaded using the service-loading technique at runtime, but not + * discoverable at build-time when processing services in the feature (see + * ServiceLoaderFeature#handleServiceClassIsReachable). This occurs when the user does not + * explicitly request a provider, but the provider is discovered via static analysis from a + * JCA-compliant security service used by the user's code (see + * SecurityServicesFeature#registerServiceReachabilityHandlers). + */ + @Platforms(Platform.HOSTED_ONLY.class)// + private final Set markedAsNotLoaded = ConcurrentHashMap.newKeySet(); + + /** Set of fully qualified provider names, required for runtime resource access. */ + private final EconomicSet userRequestedSecurityProviders = EconomicSet.create(); + + /** + * A map of providers, identified by their names (see {@link Provider#getName()}), and the + * results of their verification (see javax.crypto.JceSecurity#getVerificationResult). This + * structure is used instead of the (see javax.crypto.JceSecurity#verifyingProviders) map to + * avoid keeping provider objects in the image heap. + */ + private final EconomicMap verifiedSecurityProviders = ImageHeapMap.create("verifiedSecurityProviders"); + + private Properties savedInitialSecurityProperties; + + private Constructor sunECConstructor; + + @Platforms(Platform.HOSTED_ONLY.class) + public SecurityProvidersSupport(List userRequestedSecurityProviders) { + this.userRequestedSecurityProviders.addAll(userRequestedSecurityProviders); + } + + @Fold + public static SecurityProvidersSupport singleton() { + return ImageSingletons.lookup(SecurityProvidersSupport.class); + } + + @Platforms(Platform.HOSTED_ONLY.class) + public void addVerifiedSecurityProvider(String key, Object verificationResult) { + verifiedSecurityProviders.put(key, verificationResult); + } + + public Object getSecurityProviderVerificationResult(String key) { + return verifiedSecurityProviders.get(key); + } + + @Platforms(Platform.HOSTED_ONLY.class) + public void markSecurityProviderAsNotLoaded(String provider) { + markedAsNotLoaded.add(provider); + } + + @Platforms(Platform.HOSTED_ONLY.class) + public boolean isSecurityProviderNotLoaded(String provider) { + return markedAsNotLoaded.contains(provider); + } + + @Platforms(Platform.HOSTED_ONLY.class) + public boolean isUserRequestedSecurityProvider(String provider) { + return userRequestedSecurityProviders.contains(provider); + } + + /** + * Returns {@code true} if the provider, identified by either its name (e.g., SUN) or fully + * qualified name (e.g., sun.security.provider.Sun), is either user-requested or reachable via a + * security service. + */ + public boolean isSecurityProviderRequested(String providerName, String providerFQName) { + return verifiedSecurityProviders.containsKey(providerName) || userRequestedSecurityProviders.contains(providerFQName); + } + + @Platforms(Platform.HOSTED_ONLY.class) + public void setSunECConstructor(Constructor sunECConstructor) { + this.sunECConstructor = sunECConstructor; + } + + public Provider allocateSunECProvider() { + try { + return (Provider) sunECConstructor.newInstance(); + } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) { + throw VMError.shouldNotReachHere("The SunEC constructor is not present."); + } + } + + @Platforms(Platform.HOSTED_ONLY.class) + public void setSavedInitialSecurityProperties(Properties savedSecurityProperties) { + this.savedInitialSecurityProperties = savedSecurityProperties; + } + + public Properties getSavedInitialSecurityProperties() { + return savedInitialSecurityProperties; + } + + public Provider loadBuiltInProvider(String provName, Debug debug) { + return switch (provName) { + case "SUN", "sun.security.provider.Sun" -> + isSecurityProviderRequested("SUN", "sun.security.provider.Sun") ? new sun.security.provider.Sun() : null; + case "SunRsaSign", "sun.security.rsa.SunRsaSign" -> + isSecurityProviderRequested("SunRsaSign", "sun.security.rsa.SunRsaSign") ? new sun.security.rsa.SunRsaSign() : null; + case "SunJCE", "com.sun.crypto.provider.SunJCE" -> + isSecurityProviderRequested("SunJCE", "com.sun.crypto.provider.SunJCE") ? new com.sun.crypto.provider.SunJCE() : null; + case "SunJSSE" -> + isSecurityProviderRequested("SunJSSE", "sun.security.ssl.SunJSSE") ? new sun.security.ssl.SunJSSE() : null; + case "SunEC" -> + isSecurityProviderRequested("SunEC", "sun.security.ec.SunEC") ? allocateSunECProvider() : null; + case "Apple", "apple.security.AppleProvider" -> { + try { + Class c = Class.forName("apple.security.AppleProvider"); + if (Provider.class.isAssignableFrom(c)) { + yield (Provider) c.getDeclaredConstructor().newInstance(); + } + } catch (Exception ex) { + if (debug != null) { + debug.println("Error loading provider Apple"); + ex.printStackTrace(); + } + } + yield null; + } + default -> null; + }; + } + + public static boolean isBuiltInProvider(String provName) { + return switch (provName) { + case "SUN", "sun.security.provider.Sun", + "SunRsaSign", "sun.security.rsa.SunRsaSign", + "SunJCE", "com.sun.crypto.provider.SunJCE", + "SunJSSE", + "SunEC", + "Apple", "apple.security.AppleProvider" -> + true; + default -> false; + }; + } +} diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/SecuritySubstitutions.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/SecuritySubstitutions.java index f023a02fcc7c..fb5a71222527 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/SecuritySubstitutions.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/SecuritySubstitutions.java @@ -36,7 +36,6 @@ import java.security.Policy; import java.security.ProtectionDomain; import java.security.Provider; -import java.security.SecureRandom; import java.util.List; import java.util.Map; import java.util.Objects; @@ -244,26 +243,28 @@ private static void setJavaHome(String newJavaHome) { } } -@TargetClass(className = "javax.crypto.JceSecurity") +/** + * The {@code javax.crypto.JceSecurity#verificationResults} cache is initialized by the + * SecurityServicesFeature at build time, for all registered providers. The cache is used by + * {@code javax.crypto.JceSecurity#canUseProvider} at run time to check whether a provider is + * properly signed and can be used by JCE. It does that via jar verification which we cannot + * support. + */ +@TargetClass(className = "javax.crypto.JceSecurity", onlyWith = JDKInitializedAtBuildTime.class) @BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-24+27/src/java.base/share/classes/javax/crypto/JceSecurity.java.template") @SuppressWarnings({"unused"}) final class Target_javax_crypto_JceSecurity { - /* - * The JceSecurity.verificationResults cache is initialized by the SecurityServicesFeature at - * build time, for all registered providers. The cache is used by JceSecurity.canUseProvider() - * at runtime to check whether a provider is properly signed and can be used by JCE. It does - * that via jar verification which we cannot support. - */ - // Checkstyle: stop @Alias // private static Object PROVIDER_VERIFIED; // Checkstyle: resume - // Map of the providers we already have verified - // value == PROVIDER_VERIFIED is successfully verified - // value is failure cause Exception in error case + /* + * Map of providers that have already been verified. A value of PROVIDER_VERIFIED + * indicates successful verification. Otherwise, the value is the Exception that caused the + * verification to fail. + */ @Alias // private static Map verificationResults; @@ -281,7 +282,6 @@ final class Target_javax_crypto_JceSecurity { @Substitute static Exception getVerificationResult(Provider p) { - /* Start code block copied from original method. */ /* The verification results map key is an identity wrapper object. */ Object key = new Target_javax_crypto_JceSecurity_WeakIdentityWrapper(p, queue); Object o = verificationResults.get(key); @@ -290,19 +290,20 @@ static Exception getVerificationResult(Provider p) { } else if (o != null) { return (Exception) o; } - /* End code block copied from original method. */ /* - * If the verification result is not found in the verificationResults map JDK proceeds to - * verify it. That requires accessing the code base which we don't support. The substitution - * for getCodeBase() would be enough to take care of this too, but substituting - * getVerificationResult() allows for a better error message. + * If the verification result is not found in the verificationResults map, HotSpot will + * attempt to verify the provider. This requires accessing the code base, which isn't + * supported in Native Image, so we need to fail. We could either fail here or substitute + * getCodeBase() and fail there, but handling it here is a cleaner approach. */ - throw VMError.unsupportedFeature("Trying to verify a provider that was not registered at build time: " + p + ". " + - "All providers must be registered and verified in the Native Image builder. "); + throw new SecurityException( + "Attempted to verify a provider that was not registered at build time: " + p + ". " + + "All security providers must be registered and verified during native image generation. " + + "Try adding the option: -H:AdditionalSecurityProviders=" + p + " and rebuild the image."); } } -@TargetClass(className = "javax.crypto.JceSecurity", innerClass = "WeakIdentityWrapper") +@TargetClass(className = "javax.crypto.JceSecurity", innerClass = "WeakIdentityWrapper", onlyWith = JDKInitializedAtBuildTime.class) @SuppressWarnings({"unused"}) final class Target_javax_crypto_JceSecurity_WeakIdentityWrapper { @@ -311,31 +312,6 @@ final class Target_javax_crypto_JceSecurity_WeakIdentityWrapper { } } -class JceSecurityAccessor { - private static volatile SecureRandom RANDOM; - - static SecureRandom get() { - SecureRandom result = RANDOM; - if (result == null) { - /* Lazy initialization on first access. */ - result = initializeOnce(); - } - return result; - } - - private static synchronized SecureRandom initializeOnce() { - SecureRandom result = RANDOM; - if (result != null) { - /* Double-checked locking is OK because INSTANCE is volatile. */ - return result; - } - - result = new SecureRandom(); - RANDOM = result; - return result; - } -} - /** * JDK 8 has the class `javax.crypto.JarVerifier`, but in JDK 11 and later that class is only * available in Oracle builds, and not in OpenJDK builds. @@ -401,7 +377,7 @@ public boolean implies(ProtectionDomain domain, Permission permission) { } } -@TargetClass(className = "sun.security.jca.ProviderConfig") +@TargetClass(className = "sun.security.jca.ProviderConfig", onlyWith = JDKInitializedAtBuildTime.class) @SuppressWarnings({"unused", "static-method"}) final class Target_sun_security_jca_ProviderConfig { diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/Target_sun_security_ssl_TrustStoreManager.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/Target_sun_security_ssl_TrustStoreManager.java index 180bf4bde2f6..898c1e2efc38 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/Target_sun_security_ssl_TrustStoreManager.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/Target_sun_security_ssl_TrustStoreManager.java @@ -32,13 +32,14 @@ import org.graalvm.nativeimage.ImageSingletons; import org.graalvm.nativeimage.impl.RuntimeClassInitializationSupport; +import com.oracle.svm.core.FutureDefaultsOptions; import com.oracle.svm.core.annotate.Alias; import com.oracle.svm.core.annotate.Delete; import com.oracle.svm.core.annotate.RecomputeFieldValue; import com.oracle.svm.core.annotate.Substitute; import com.oracle.svm.core.annotate.TargetClass; -import com.oracle.svm.core.feature.InternalFeature; import com.oracle.svm.core.feature.AutomaticallyRegisteredFeature; +import com.oracle.svm.core.feature.InternalFeature; import com.oracle.svm.core.util.VMError; import com.oracle.svm.util.ReflectionUtil; @@ -89,13 +90,15 @@ public void afterRegistration(AfterRegistrationAccess access) { */ RuntimeClassInitializationSupport rci = ImageSingletons.lookup(RuntimeClassInitializationSupport.class); rci.initializeAtBuildTime("sun.security.util.UntrustedCertificates", "Required for TrustStoreManager"); - /* - * All security providers must be registered (and initialized) at buildtime (see - * SecuritySubstitutions.java). XMLDSigRI is used for validating XML Signatures from - * certificate files while generating X509Certificates. - */ - rci.initializeAtBuildTime("org.jcp.xml.dsig.internal.dom.XMLDSigRI", "Required for TrustStoreManager"); - rci.initializeAtBuildTime("org.jcp.xml.dsig.internal.dom.XMLDSigRI$ProviderService", "Required for TrustStoreManager"); + if (!FutureDefaultsOptions.isJDKInitializedAtRunTime()) { + /* + * All security providers must be registered (and initialized) at buildtime (see + * SecuritySubstitutions.java). XMLDSigRI is used for validating XML Signatures from + * certificate files while generating X509Certificates. + */ + rci.initializeAtBuildTime("org.jcp.xml.dsig.internal.dom.XMLDSigRI", "Required for TrustStoreManager"); + rci.initializeAtBuildTime("org.jcp.xml.dsig.internal.dom.XMLDSigRI$ProviderService", "Required for TrustStoreManager"); + } } } diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/runtimeinit/SecuritySubstitutionRuntimeInit.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/runtimeinit/SecuritySubstitutionRuntimeInit.java new file mode 100644 index 000000000000..5565d80d5716 --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/runtimeinit/SecuritySubstitutionRuntimeInit.java @@ -0,0 +1,187 @@ +/* + * Copyright (c) 2025, 2025, 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.runtimeinit; + +import java.net.URL; +import java.security.Provider; +import java.util.Map; +import java.util.Properties; +import java.util.WeakHashMap; + +import com.oracle.svm.core.annotate.Alias; +import com.oracle.svm.core.annotate.RecomputeFieldValue; +import com.oracle.svm.core.annotate.Substitute; +import com.oracle.svm.core.annotate.TargetClass; +import com.oracle.svm.core.jdk.JDKInitializedAtRunTime; +import com.oracle.svm.core.jdk.SecurityProvidersSupport; +import com.oracle.svm.core.util.BasedOnJDKFile; + +import jdk.graal.compiler.core.common.SuppressFBWarnings; + +@TargetClass(value = java.security.Security.class, onlyWith = JDKInitializedAtRunTime.class) +final class Target_java_security_Security { + @Alias // + @RecomputeFieldValue(kind = RecomputeFieldValue.Kind.FromAlias) // + static Properties props; +} + +@TargetClass(value = java.security.Security.class, innerClass = "SecPropLoader", onlyWith = JDKInitializedAtRunTime.class) +final class Target_java_security_Security_SecPropLoader { + + /** + * On HotSpot, this method loads the properties from the JDK's default location. Since we do not + * have a full JDK at run time, we use a snapshot of these values captured at build time from + * the host JVM. + */ + @Substitute + private static void loadMaster() { + Target_java_security_Security.props = SecurityProvidersSupport.singleton().getSavedInitialSecurityProperties(); + } +} + +/** + * The {@code javax.crypto.JceSecurity#verificationResults} cache is initialized by the + * SecurityServicesFeature at build time, for all registered providers. The cache is used by + * {@code javax.crypto.JceSecurity#canUseProvider} at run time to check whether a provider is + * properly signed and can be used by JCE. It does that via jar verification which we cannot + * support. + */ +@TargetClass(className = "javax.crypto.JceSecurity", onlyWith = JDKInitializedAtRunTime.class) +@BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-24+27/src/java.base/share/classes/javax/crypto/JceSecurity.java.template") +@SuppressWarnings({"unused"}) +final class Target_javax_crypto_JceSecurity { + + /* + * Map of providers that have already been verified. A value of PROVIDER_VERIFIED + * indicates successful verification. Otherwise, the value is the Exception that caused the + * verification to fail. + */ + @Alias // + @RecomputeFieldValue(kind = RecomputeFieldValue.Kind.Reset) // + private static Map verificationResults; + + @Alias // + @RecomputeFieldValue(kind = RecomputeFieldValue.Kind.Reset) // + private static Map verifyingProviders; + + @Alias // + @RecomputeFieldValue(kind = RecomputeFieldValue.Kind.FromAlias) // + private static Map, URL> codeBaseCacheRef = new WeakHashMap<>(); + + @Substitute + static Exception getVerificationResult(Provider p) { + /* The verification results map key is an identity wrapper object. */ + Object o = SecurityProvidersSupport.singleton().getSecurityProviderVerificationResult(p.getName()); + if (o == Boolean.TRUE) { + return null; + } else if (o != null) { + return (Exception) o; + } + /* + * If the verification result is not found in the verificationResults map, HotSpot will + * attempt to verify the provider. This requires accessing the code base, which isn't + * supported in Native Image, so we need to fail. We could either fail here or substitute + * getCodeBase() and fail there, but handling it here is a cleaner approach. + */ + throw new SecurityException( + "Attempted to verify a provider that was not registered at build time: " + p + ". " + + "All security providers must be registered and verified during native image generation. " + + "Try adding the option: -H:AdditionalSecurityProviders=" + p + " and rebuild the image."); + } +} + +@TargetClass(className = "sun.security.jca.ProviderConfig", onlyWith = JDKInitializedAtRunTime.class) +@SuppressWarnings({"unused", "static-method"}) +final class Target_sun_security_jca_ProviderConfig { + + @Alias // + private String provName; + + @Alias// + private static sun.security.util.Debug debug; + + @Alias// + private Provider provider; + + @Alias// + private boolean isLoading; + + @Alias// + private int tries; + + @Alias + private native Provider doLoadProvider(); + + @Alias + private native boolean shouldLoad(); + + /** + * The `entrypoint` for allocating security providers at runtime. The implementation is copied + * from the JDK with a small tweak to filter out providers that are neither user-requested nor + * reachable via a security service. + */ + @Substitute + @SuppressFBWarnings(value = "DC_DOUBLECHECK", justification = "This double-check is implemented correctly and is intentional.") + Provider getProvider() { + if (provider != null) { + return provider; + } + synchronized (this) { + if (provider != null) { + return provider; + } + if (!shouldLoad()) { + return null; + } + // Create providers which are in java.base directly + if (SecurityProvidersSupport.isBuiltInProvider(provName)) { + provider = SecurityProvidersSupport.singleton().loadBuiltInProvider(provName, debug); + } else { + if (isLoading) { + /* + * This method is synchronized, so this can only happen if there is recursion. + */ + if (debug != null) { + debug.println("Recursion loading provider: " + this); + new Exception("Call trace").printStackTrace(); + } + return null; + } + try { + isLoading = true; + tries++; + provider = doLoadProvider(); + } finally { + isLoading = false; + } + } + } + return provider; + } +} + +public class SecuritySubstitutionRuntimeInit { +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/SecurityServicesFeature.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/SecurityServicesFeature.java index 7334c354f2a8..9460234bb9d4 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/SecurityServicesFeature.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/SecurityServicesFeature.java @@ -32,6 +32,7 @@ import java.io.IOException; import java.io.PrintWriter; import java.lang.ref.WeakReference; +import java.lang.reflect.Constructor; import java.lang.reflect.Executable; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; @@ -63,6 +64,7 @@ import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.Properties; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.function.BiConsumer; @@ -87,6 +89,7 @@ import com.oracle.graal.pointsto.meta.AnalysisMethod; import com.oracle.graal.pointsto.reports.ReportUtils; import com.oracle.svm.core.BuildPhaseProvider; +import com.oracle.svm.core.FutureDefaultsOptions; import com.oracle.svm.core.SubstrateOptions; import com.oracle.svm.core.SubstrateUtil; import com.oracle.svm.core.feature.AutomaticallyRegisteredFeature; @@ -95,6 +98,8 @@ import com.oracle.svm.core.jdk.JNIRegistrationUtil; import com.oracle.svm.core.jdk.NativeLibrarySupport; import com.oracle.svm.core.jdk.PlatformNativeLibrarySupport; +import com.oracle.svm.core.jdk.SecurityProvidersSupport; +import com.oracle.svm.core.jdk.SecuritySubstitutions; import com.oracle.svm.core.option.AccumulatingLocatableMultiOptionValue; import com.oracle.svm.core.option.HostedOptionKey; import com.oracle.svm.core.util.UserError; @@ -111,10 +116,38 @@ import jdk.graal.compiler.debug.Assertions; import jdk.graal.compiler.options.Option; +import jdk.internal.access.SharedSecrets; import sun.security.jca.ProviderList; import sun.security.provider.NativePRNG; import sun.security.x509.OIDMap; +/** + *

+ * This feature automatically registers security providers and their services for reflection and JNI + * access, ensuring they are available at run time. + * + *

+ * The feature distinguishes between providers that are initialized at build time and those that are + * initialized at run time. This distinction is essential because certain providers may perform + * sensitive operations. Right now, all providers are initialized build-time by default, but that + * can be changed using --future-defaults=all or --future-defaults=run-time-initialized-jdk + * + *

+ * The initialization strategy is: + *

    + *
  • Build-time Initialization: Most cryptographic infrastructure is initialized at build-time. + * This includes reflection metadata and service registration.
  • + *
  • Run-time Initialization: Classes that rely on system resources (e.g., {@code /dev/urandom}, + * keystore passwords, or native Windows libraries) are marked for runtime initialization or the + * providers (if --future-defaults is used).
  • + *
+ * + *

+ * This feature is automatically registered, but it can be controlled via the + * {@code EnableSecurityServicesFeature} option. For debugging or detailed inspection, tracing can + * be enabled via the {@code TraceSecurityServices} option. + */ + @AutomaticallyRegisteredFeature public class SecurityServicesFeature extends JNIRegistrationUtil implements InternalFeature { @@ -131,7 +164,8 @@ public static class Options { @Option(help = "Comma-separated list of additional security provider fully qualified class names to mark as used." + "Note that this option is only necessary if you use custom engine classes not available in JCA that are not JCA compliant.")// - public static final HostedOptionKey AdditionalSecurityProviders = new HostedOptionKey<>(AccumulatingLocatableMultiOptionValue.Strings.build()); + public static final HostedOptionKey AdditionalSecurityProviders = new HostedOptionKey<>( + AccumulatingLocatableMultiOptionValue.Strings.buildWithCommaDelimiter()); } /* @@ -217,6 +251,10 @@ public static class Options { @Override public void afterRegistration(AfterRegistrationAccess a) { + if (FutureDefaultsOptions.isJDKInitializedAtRunTime()) { + ImageSingletons.add(SecurityProvidersSupport.class, new SecurityProvidersSupport(Options.AdditionalSecurityProviders.getValue().values())); + } + ModuleSupport.accessPackagesToClass(ModuleSupport.Access.OPEN, getClass(), false, "java.base", "sun.security.x509"); ModuleSupport.accessModuleByClass(ModuleSupport.Access.OPEN, getClass(), Security.class); ImageSingletons.lookup(RuntimeClassInitializationSupport.class).initializeAtBuildTime("javax.security.auth.kerberos.KeyTab", @@ -226,16 +264,35 @@ public void afterRegistration(AfterRegistrationAccess a) { @Override public void duringSetup(DuringSetupAccess a) { DuringSetupAccessImpl access = (DuringSetupAccessImpl) a; - addManuallyConfiguredUsedProviders(a); - - verificationResultsField = access.findField("javax.crypto.JceSecurity", "verificationResults"); - providerListField = access.findField("sun.security.jca.Providers", "providerList"); + RuntimeClassInitializationSupport rci = ImageSingletons.lookup(RuntimeClassInitializationSupport.class); oidTableField = access.findField("sun.security.util.ObjectIdentifier", "oidTable"); oidMapField = access.findField(OIDMap.class, "oidMap"); - classCacheField = access.findField(Service.class, "classCache"); - constructorCacheField = access.findField(Service.class, "constructorCache"); + if (!FutureDefaultsOptions.isJDKInitializedAtRunTime()) { + addManuallyConfiguredUsedProviders(a); + verificationResultsField = access.findField("javax.crypto.JceSecurity", "verificationResults"); + providerListField = access.findField("sun.security.jca.Providers", "providerList"); + classCacheField = access.findField(Service.class, "classCache"); + constructorCacheField = access.findField(Service.class, "constructorCache"); + } else { + SecurityProvidersSupport support = SecurityProvidersSupport.singleton(); + ModuleSupport.accessPackagesToClass(ModuleSupport.Access.OPEN, SecuritySubstitutions.class, false, "java.base", "sun.security.ec"); + Constructor sunECConstructor = constructor(a, "sun.security.ec.SunEC"); + support.setSunECConstructor(sunECConstructor); + + Properties securityProperties = SharedSecrets.getJavaSecurityPropertiesAccess().getInitialProperties(); + support.setSavedInitialSecurityProperties(securityProperties); + + /* + * For the three providers below, their packages are explicitly initialized at build + * time in JdkInitializationFeature#afterRegistration. This unnecessarily includes them + * in the build-time initialization, so we need to mark them explicitly for run-time + * initialization instead. + */ + rci.initializeAtRunTime("java.security.Security", FutureDefaultsOptions.RUN_TIME_INITIALIZE_JDK_REASON); + rci.initializeAtRunTime("sun.security.jca.Providers", FutureDefaultsOptions.RUN_TIME_INITIALIZE_JDK_REASON); + rci.initializeAtRunTime("sun.security.provider.certpath.ldap.JdkLDAP", FutureDefaultsOptions.RUN_TIME_INITIALIZE_JDK_REASON); + } - RuntimeClassInitializationSupport rci = ImageSingletons.lookup(RuntimeClassInitializationSupport.class); /* * The SecureRandom implementations open the /dev/random and /dev/urandom files which are * used as sources for entropy. These files are opened in the static initializers. @@ -278,7 +335,7 @@ public void duringSetup(DuringSetupAccess a) { rci.initializeAtRunTime(clazz(access, "sun.security.ssl.SSLContextImpl$DefaultManagersHolder"), "for reading properties at run time"); /* - * SSL debug logging enabled by javax.net.debug system property is setup during the class + * SSL debug logging enabled by javax.net.debug system property is set up during the class * initialization. */ rci.initializeAtRunTime(clazz(access, "sun.security.ssl.SSLLogger"), "for reading properties at run time"); @@ -333,62 +390,64 @@ public void beforeAnalysis(BeforeAnalysisAccess a) { PlatformNativeLibrarySupport.singleton().addBuiltinPkgNativePrefix("sun_security_mscapi"); } - substitutionProcessor = ((Inflation) access.getBigBang()).getAnnotationSubstitutionProcessor(); + if (!FutureDefaultsOptions.isJDKInitializedAtRunTime()) { + substitutionProcessor = ((Inflation) access.getBigBang()).getAnnotationSubstitutionProcessor(); - access.registerFieldValueTransformer(providerListField, new FieldValueTransformerWithAvailability() { - /* - * We must wait until all providers have been registered before filtering the list. - */ - @Override - public boolean isAvailable() { - return BuildPhaseProvider.isHostedUniverseBuilt(); - } + access.registerFieldValueTransformer(providerListField, new FieldValueTransformerWithAvailability() { + /* + * We must wait until all providers have been registered before filtering the list. + */ + @Override + public boolean isAvailable() { + return BuildPhaseProvider.isHostedUniverseBuilt(); + } - @Override - public Object transform(Object receiver, Object originalValue) { - if (cachedProviders != null) { - if (SubstrateUtil.assertionsEnabled()) { - var filteredProviders = filterProviderList(originalValue); - assert cachedProviders.providers().equals(filteredProviders) : Assertions.errorMessage(cachedProviders.providers(), filteredProviders); - } - if (Options.TraceSecurityServices.getValue()) { - ProviderList providerList = (ProviderList) originalValue; - List removedProviders = providerList.providers().stream().filter(p -> shouldRemoveProvider(p)).toList(); - traceRemovedProviders(removedProviders); + @Override + public Object transform(Object receiver, Object originalValue) { + if (cachedProviders != null) { + if (SubstrateUtil.assertionsEnabled()) { + var filteredProviders = filterProviderList(originalValue); + assert cachedProviders.providers().equals(filteredProviders) : Assertions.errorMessage(cachedProviders.providers(), filteredProviders); + } + if (Options.TraceSecurityServices.getValue()) { + ProviderList providerList = (ProviderList) originalValue; + List removedProviders = providerList.providers().stream().filter(p -> shouldRemoveProvider(p)).toList(); + traceRemovedProviders(removedProviders); + } } + /* + * This object is manually rescanned during analysis to ensure its entire type + * structure is part of the analysis universe. + */ + return cachedProviders; } + }); + + access.registerFieldValueTransformer(verificationResultsField, new FieldValueTransformerWithAvailability() { /* - * This object is manually rescanned during analysis to ensure its entire type - * structure is part of the analysis universe. + * We must wait until all providers have been registered before filtering the list. */ - return cachedProviders; - } - }); - - access.registerFieldValueTransformer(verificationResultsField, new FieldValueTransformerWithAvailability() { - /* - * We must wait until all providers have been registered before filtering the list. - */ - @Override - public boolean isAvailable() { - return BuildPhaseProvider.isHostedUniverseBuilt(); - } + @Override + public boolean isAvailable() { + return BuildPhaseProvider.isHostedUniverseBuilt(); + } - @Override - public Object transform(Object receiver, Object originalValue) { - if (cachedVerificationCache != null) { - if (SubstrateUtil.assertionsEnabled()) { - var filteredCache = filterVerificationCache(originalValue); - assert cachedVerificationCache.equals(filteredCache) : Assertions.errorMessage(cachedVerificationCache, filteredCache); + @Override + public Object transform(Object receiver, Object originalValue) { + if (cachedVerificationCache != null) { + if (SubstrateUtil.assertionsEnabled()) { + var filteredCache = filterVerificationCache(originalValue); + assert cachedVerificationCache.equals(filteredCache) : Assertions.errorMessage(cachedVerificationCache, filteredCache); + } } + /* + * This object is manually rescanned during analysis to ensure its entire type + * structure is part of the analysis universe. + */ + return cachedVerificationCache; } - /* - * This object is manually rescanned during analysis to ensure its entire type - * structure is part of the analysis universe. - */ - return cachedVerificationCache; - } - }); + }); + } } @SuppressWarnings("unchecked") @@ -601,7 +660,7 @@ private void registerServices(DuringAnalysisAccess access, Object trigger, Class /* * SPI classes, i.e., base classes for concrete service implementations, such as * java.security.MessageDigestSpi, can be dynamically loaded to double-check the base type - * of a newly allocated SPI object. This only applies to SPIs in the java.security package, + * of newly allocated SPI object. This only applies to SPIs in the java.security package, * but not any of its sub-packages. See java.security.Security.getSpiClass(). */ String serviceType = getServiceType(serviceClass); @@ -690,7 +749,7 @@ private static Function> getConstructorParameterClassAccessor(I Field consParamClassField = ReflectionUtil.lookupField(clazz, "constructorParameterClass"); /* - * The returned lambda captures the value of the Provider.knownEngines map retrieved above + * The returned lambda captures the value of the Provider.knownEngines map retrieved above, * and it uses it to find the parameterClass corresponding to the serviceType parameter. */ return (serviceType) -> { @@ -745,7 +804,7 @@ private static void registerSpiClass(Method getSpiClassMethod, String serviceTyp } } - private void registerProvider(Provider provider) { + private void registerProvider(DuringAnalysisAccess access, Provider provider) { if (usedProviders.add(provider)) { registerForReflection(provider.getClass()); /* Trigger initialization of lazy field java.security.Provider.entrySet. */ @@ -758,7 +817,32 @@ private void registerProvider(Provider provider) { * signed and can be used by JCE. It does that via jar verification which we cannot * support. See also Target_javax_crypto_JceSecurity. */ - getVerificationResult.invoke(null, provider); + Object result = getVerificationResult.invoke(null, provider); + if (FutureDefaultsOptions.isJDKInitializedAtRunTime()) { + /* + * Note that after verification, we move the result to a separate structure + * since we don't want to keep the provider object in the image heap. + * + * The verification result can be either null, in case of success, or an + * Exception, in case of failure. Null is interpreted as Boolean.TRUE at + * runtime, signifying successful verification. + */ + String providerName = provider.getName(); + SecurityProvidersSupport support = SecurityProvidersSupport.singleton(); + support.addVerifiedSecurityProvider(providerName, result instanceof Exception ? result : Boolean.TRUE); + + /* + * If this provider is not yet loaded via the service loading mechanism, we need + * to manually prepare reflection metadata now, so that service loading works at + * runtime (see sun.security.jca.ProviderConfig.doLoadProvider). + */ + String providerFQName = provider.getClass().getName(); + if (support.isSecurityProviderNotLoaded(providerFQName)) { + Set registeredProviders = new HashSet<>(); + ServiceLoaderFeature.registerProviderForRuntimeReflectionAccess(access, providerFQName, registeredProviders); + ServiceLoaderFeature.registerProviderForRuntimeResourceAccess(access.getApplicationClassLoader().getUnnamedModule(), Provider.class.getName(), registeredProviders); + } + } } catch (ReflectiveOperationException ex) { throw VMError.shouldNotReachHere(ex); } @@ -789,7 +873,7 @@ private void registerService(DuringAnalysisAccess a, Service service) { if (isCertificateFactory(service) && service.getAlgorithm().equals(X509)) { registerX509Extensions(a); } - registerProvider(service.getProvider()); + registerProvider(a, service.getProvider()); } } else { trace("Cannot register service %s. Reason: %s.", asString(service), serviceClassResult.getException()); @@ -798,7 +882,7 @@ private void registerService(DuringAnalysisAccess a, Service service) { /** * Register the default JavaKeyStore, JKS, for reflection. It is not registered as a key store - * implementation in any provider but it is registered as a primary key store for + * implementation in any provider, but it is registered as a primary key store for * JavaKeyStore$DualFormatJKS, i.e., the KeyStore.JKS implementation class in the SUN provider, * and dynamically allocated by sun.security.provider.KeyStoreDelegator.engineLoad(). */ @@ -835,14 +919,16 @@ private void registerX509Extensions(DuringAnalysisAccess a) { @Override public void duringAnalysis(DuringAnalysisAccess a) { DuringAnalysisAccessImpl access = (DuringAnalysisAccessImpl) a; - maybeScanVerificationResultsField(access); - maybeScanProvidersField(access); access.rescanRoot(oidTableField); - if (cachedProviders != null) { - for (Provider provider : cachedProviders.providers()) { - for (Service service : provider.getServices()) { - access.rescanField(service, classCacheField); - access.rescanField(service, constructorCacheField); + if (!FutureDefaultsOptions.isJDKInitializedAtRunTime()) { + maybeScanVerificationResultsField(access); + maybeScanProvidersField(access); + if (cachedProviders != null) { + for (Provider provider : cachedProviders.providers()) { + for (Service service : provider.getServices()) { + access.rescanField(service, classCacheField); + access.rescanField(service, constructorCacheField); + } } } } 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 0dfaeed8752c..dc54ab3d24af 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 @@ -37,26 +37,28 @@ import java.util.stream.Collectors; import java.util.stream.Stream; -import jdk.graal.compiler.hotspot.CompilerConfigurationFactory; -import jdk.graal.compiler.hotspot.HotSpotBackendFactory; -import jdk.graal.compiler.hotspot.meta.DefaultHotSpotLoweringProvider; -import jdk.graal.compiler.hotspot.meta.HotSpotInvocationPluginProvider; -import jdk.graal.compiler.truffle.hotspot.TruffleCallBoundaryInstrumentationFactory; -import jdk.vm.ci.hotspot.HotSpotJVMCIBackendFactory; import org.graalvm.nativeimage.Platform; import org.graalvm.nativeimage.hosted.RuntimeReflection; import org.graalvm.nativeimage.hosted.RuntimeResourceAccess; +import com.oracle.svm.core.FutureDefaultsOptions; import com.oracle.svm.core.feature.AutomaticallyRegisteredFeature; import com.oracle.svm.core.feature.InternalFeature; +import com.oracle.svm.core.jdk.SecurityProvidersSupport; import com.oracle.svm.core.jdk.ServiceCatalogSupport; import com.oracle.svm.core.option.AccumulatingLocatableMultiOptionValue; import com.oracle.svm.core.option.HostedOptionKey; import com.oracle.svm.core.util.BasedOnJDKFile; import com.oracle.svm.hosted.analysis.Inflation; +import jdk.graal.compiler.hotspot.CompilerConfigurationFactory; +import jdk.graal.compiler.hotspot.HotSpotBackendFactory; +import jdk.graal.compiler.hotspot.meta.DefaultHotSpotLoweringProvider; +import jdk.graal.compiler.hotspot.meta.HotSpotInvocationPluginProvider; import jdk.graal.compiler.options.Option; import jdk.graal.compiler.options.OptionType; +import jdk.graal.compiler.truffle.hotspot.TruffleCallBoundaryInstrumentationFactory; +import jdk.vm.ci.hotspot.HotSpotJVMCIBackendFactory; import sun.util.locale.provider.LocaleDataMetaInfo; /** @@ -114,7 +116,6 @@ public static class Options { * initialized at image build time. */ RandomGenerator.class, - java.security.Provider.class, // see SecurityServicesFeature LocaleDataMetaInfo.class, // see LocaleSubstitutions /* Graal hotspot-specific services */ @@ -148,6 +149,9 @@ public boolean isInConfiguration(IsInConfigurationAccess access) { @Override public void afterRegistration(AfterRegistrationAccess access) { + if (!FutureDefaultsOptions.isJDKInitializedAtRunTime()) { + servicesToSkip.add(java.security.Provider.class.getName()); + } servicesToSkip.addAll(Options.ServiceLoaderFeatureExcludeServices.getValue().values()); serviceProvidersToSkip.addAll(Options.ServiceLoaderFeatureExcludeServiceProviders.getValue().values()); } @@ -180,81 +184,92 @@ public void beforeAnalysis(BeforeAnalysisAccess access) { }); } - @BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-25+11/src/java.base/share/classes/java/util/ServiceLoader.java#L745-L793") void handleServiceClassIsReachable(DuringAnalysisAccess access, Class serviceProvider, Collection providers) { LinkedHashSet registeredProviders = new LinkedHashSet<>(); for (String provider : providers) { if (serviceProvidersToSkip.contains(provider)) { continue; } - /* Make provider reflectively instantiable */ - Class providerClass = access.findClassByName(provider); - - if (providerClass == null || providerClass.isArray() || providerClass.isPrimitive()) { - continue; - } - FeatureImpl.DuringAnalysisAccessImpl accessImpl = (FeatureImpl.DuringAnalysisAccessImpl) access; - if (!accessImpl.getHostVM().platformSupported(providerClass)) { - continue; - } - if (((Inflation) accessImpl.getBigBang()).getAnnotationSubstitutionProcessor().isDeleted(providerClass)) { - /* Disallow services with implementation classes that are marked as @Deleted */ - continue; + if (serviceProvider.equals(java.security.Provider.class) && !SecurityProvidersSupport.singleton().isUserRequestedSecurityProvider(provider)) { + SecurityProvidersSupport.singleton().markSecurityProviderAsNotLoaded(provider); + } else { + registerProviderForRuntimeReflectionAccess(access, provider, registeredProviders); } + } + registerProviderForRuntimeResourceAccess(access.getApplicationClassLoader().getUnnamedModule(), serviceProvider.getName(), registeredProviders); + } - /* - * Find either a public static provider() method or a nullary constructor (or both). - * Skip providers that do not comply with requirements. - * - * See ServiceLoader#loadProvider and ServiceLoader#findStaticProviderMethod. - */ - Method nullaryProviderMethod = findProviderMethod(providerClass); - Constructor nullaryConstructor = findNullaryConstructor(providerClass); - if (nullaryConstructor != null || nullaryProviderMethod != null) { - RuntimeReflection.register(providerClass); - if (nullaryConstructor != null) { - /* - * Registering a constructor with - * RuntimeReflection.registerConstructorLookup(providerClass) does not produce - * the same behavior as using RuntimeReflection.register(nullaryConstructor). In - * the first case, the constructor is marked for query purposes only, so this - * if-statement cannot be eliminated. - * - */ - RuntimeReflection.register(nullaryConstructor); - } else { - /* - * If there's no nullary constructor, register it as negative lookup to avoid - * throwing a MissingReflectionRegistrationError at run time. - */ - RuntimeReflection.registerConstructorLookup(providerClass); - } - if (nullaryProviderMethod != null) { - RuntimeReflection.register(nullaryProviderMethod); - } else { - /* - * If there's no declared public provider() method, register it as negative - * lookup to avoid throwing a MissingReflectionRegistrationError at run time. - */ - RuntimeReflection.registerMethodLookup(providerClass, "provider"); - } + @BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-25+21/src/java.base/share/classes/java/util/ServiceLoader.java#L745-L793") + public static void registerProviderForRuntimeReflectionAccess(DuringAnalysisAccess access, String provider, Set registeredProviders) { + /* Make provider reflectively instantiable */ + Class providerClass = access.findClassByName(provider); + + if (providerClass == null || providerClass.isArray() || providerClass.isPrimitive()) { + return; + } + FeatureImpl.DuringAnalysisAccessImpl accessImpl = (FeatureImpl.DuringAnalysisAccessImpl) access; + if (!accessImpl.getHostVM().platformSupported(providerClass)) { + return; + } + if (((Inflation) accessImpl.getBigBang()).getAnnotationSubstitutionProcessor().isDeleted(providerClass)) { + /* Disallow services with implementation classes that are marked as @Deleted */ + return; + } + + /* + * Find either a public static provider() method or a nullary constructor (or both). Skip + * providers that do not comply with requirements. + * + * See ServiceLoader#loadProvider and ServiceLoader#findStaticProviderMethod. + */ + Method nullaryProviderMethod = findProviderMethod(providerClass); + Constructor nullaryConstructor = findNullaryConstructor(providerClass); + if (nullaryConstructor != null || nullaryProviderMethod != null) { + RuntimeReflection.register(providerClass); + if (nullaryConstructor != null) { + /* + * Registering a constructor with + * RuntimeReflection.registerConstructorLookup(providerClass) does not produce the + * same behavior as using RuntimeReflection.register(nullaryConstructor). In the + * first case, the constructor is marked for query purposes only, so this + * if-statement cannot be eliminated. + * + */ + RuntimeReflection.register(nullaryConstructor); + } else { + /* + * If there's no nullary constructor, register it as negative lookup to avoid + * throwing a MissingReflectionRegistrationError at run time. + */ + RuntimeReflection.registerConstructorLookup(providerClass); + } + if (nullaryProviderMethod != null) { + RuntimeReflection.register(nullaryProviderMethod); + } else { + /* + * If there's no declared public provider() method, register it as negative lookup + * to avoid throwing a MissingReflectionRegistrationError at run time. + */ + RuntimeReflection.registerMethodLookup(providerClass, "provider"); } - /* - * Register the provider in both cases: when it is JCA-compliant (has a nullary - * constructor or a provider method) or when it lacks both. If neither is present, a - * ServiceConfigurationError will be thrown at runtime, consistent with HotSpot - * behavior. - */ - registeredProviders.add(provider); } + /* + * Register the provider in both cases: when it is JCA-compliant (has a nullary constructor + * or a provider method) or when it lacks both. If neither is present, a + * ServiceConfigurationError will be thrown at runtime, consistent with HotSpot behavior. + */ + registeredProviders.add(provider); + } + + public static void registerProviderForRuntimeResourceAccess(Module module, String serviceProviderName, Set registeredProviders) { if (!registeredProviders.isEmpty()) { - String serviceResourceLocation = "META-INF/services/" + serviceProvider.getName(); - byte[] serviceFileData = registeredProviders.stream().collect(Collectors.joining("\n")).getBytes(StandardCharsets.UTF_8); - RuntimeResourceAccess.addResource(access.getApplicationClassLoader().getUnnamedModule(), serviceResourceLocation, serviceFileData); + String serviceResourceLocation = "META-INF/services/" + serviceProviderName; + byte[] serviceFileData = String.join("\n", registeredProviders).getBytes(StandardCharsets.UTF_8); + RuntimeResourceAccess.addResource(module, serviceResourceLocation, serviceFileData); } } - @BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-25+11/src/java.base/share/classes/java/util/ServiceLoader.java#L620-L631") + @BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-25+21/src/java.base/share/classes/java/util/ServiceLoader.java#L620-L631") private static Constructor findNullaryConstructor(Class providerClass) { Constructor nullaryConstructor = null; try { @@ -268,7 +283,7 @@ private static Constructor findNullaryConstructor(Class providerClass) { return nullaryConstructor; } - @BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-25+11/src/java.base/share/classes/java/util/ServiceLoader.java#L583-L612") + @BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-25+21/src/java.base/share/classes/java/util/ServiceLoader.java#L583-L612") private static Method findProviderMethod(Class providerClass) { Method nullaryProviderMethod = null; try { diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/jdk/JDKInitializationFeature.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/jdk/JDKInitializationFeature.java index 86dfa00e9f2d..b83441fe2ef4 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/jdk/JDKInitializationFeature.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/jdk/JDKInitializationFeature.java @@ -196,7 +196,6 @@ public void afterRegistration(AfterRegistrationAccess access) { rci.initializeAtBuildTime("com.sun.security.jgss", JDK_CLASS_REASON); rci.initializeAtBuildTime("com.sun.security.cert.internal.x509", JDK_CLASS_REASON); rci.initializeAtBuildTime("com.sun.security.ntlm", JDK_CLASS_REASON); - rci.initializeAtBuildTime("com.sun.security.sasl", JDK_CLASS_REASON); rci.initializeAtBuildTime("java.security", JDK_CLASS_REASON); rci.initializeAtRunTime("sun.security.pkcs11.P11Util", "Cleaner reference"); @@ -218,7 +217,6 @@ public void afterRegistration(AfterRegistrationAccess access) { rci.initializeAtBuildTime("sun.security.krb5", JDK_CLASS_REASON); rci.initializeAtBuildTime("sun.security.pkcs", JDK_CLASS_REASON); rci.initializeAtBuildTime("sun.security.pkcs10", JDK_CLASS_REASON); - rci.initializeAtBuildTime("sun.security.pkcs11", JDK_CLASS_REASON); rci.initializeAtBuildTime("sun.security.pkcs12", JDK_CLASS_REASON); rci.initializeAtBuildTime("sun.security.provider", JDK_CLASS_REASON); rci.initializeAtBuildTime("sun.security.rsa", JDK_CLASS_REASON); @@ -228,8 +226,12 @@ public void afterRegistration(AfterRegistrationAccess access) { rci.initializeAtBuildTime("sun.security.util", JDK_CLASS_REASON); rci.initializeAtBuildTime("sun.security.validator", JDK_CLASS_REASON); rci.initializeAtBuildTime("sun.security.x509", JDK_CLASS_REASON); - rci.initializeAtBuildTime("sun.security.smartcardio", JDK_CLASS_REASON); rci.initializeAtBuildTime("com.sun.jndi", JDK_CLASS_REASON); + if (!FutureDefaultsOptions.isJDKInitializedAtRunTime()) { + rci.initializeAtBuildTime("sun.security.pkcs11", JDK_CLASS_REASON); + rci.initializeAtBuildTime("sun.security.smartcardio", JDK_CLASS_REASON); + rci.initializeAtBuildTime("com.sun.security.sasl", JDK_CLASS_REASON); + } if (Platform.includedIn(Platform.DARWIN.class)) { rci.initializeAtBuildTime("apple.security", JDK_CLASS_REASON); } diff --git a/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/services/SecurityServiceTest.java b/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/services/SecurityServiceTest.java index c24493f494a0..904aebf36333 100644 --- a/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/services/SecurityServiceTest.java +++ b/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/services/SecurityServiceTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2019, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2019, 2025, 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 @@ -34,8 +34,10 @@ import org.hamcrest.CoreMatchers; import org.hamcrest.MatcherAssert; import org.junit.Assert; +import org.junit.Assume; import org.junit.Test; +import com.oracle.svm.core.FutureDefaultsOptions; import com.oracle.svm.core.annotate.Delete; import com.oracle.svm.core.annotate.TargetClass; import com.oracle.svm.util.ModuleSupport; @@ -59,11 +61,12 @@ public void afterRegistration(AfterRegistrationAccess access) { @Override public void duringSetup(final DuringSetupAccess access) { - // we use these (application) classes during Native image build - RuntimeClassInitialization.initializeAtBuildTime(NoOpService.class); - RuntimeClassInitialization.initializeAtBuildTime(NoOpProvider.class); - RuntimeClassInitialization.initializeAtBuildTime(NoOpProviderTwo.class); - + if (!FutureDefaultsOptions.isJDKInitializedAtRunTime()) { + // we use these (application) classes during Native image build + RuntimeClassInitialization.initializeAtBuildTime(NoOpService.class); + RuntimeClassInitialization.initializeAtBuildTime(NoOpProvider.class); + RuntimeClassInitialization.initializeAtBuildTime(NoOpProviderTwo.class); + } // register the service implementation for reflection explicitly, // non-standard services are not processed automatically RuntimeReflection.register(NoOpImpl.class); @@ -71,6 +74,22 @@ public void duringSetup(final DuringSetupAccess access) { } } + /** + * This test ensures that the list of security providers is populated at run time, and not at + * build time. + */ + @Test + public void testSecurityProviderRuntimeRegistration() { + Assume.assumeTrue("needs runtime initialization", FutureDefaultsOptions.isJDKInitializedAtRunTime()); + Provider notRegistered = Security.getProvider("no-op-provider"); + Assert.assertNull("Provider is registered.", notRegistered); + + Security.addProvider(new NoOpProvider()); + + Provider registered = Security.getProvider("no-op-provider"); + Assert.assertNotNull("Provider is not registered.", registered); + } + /** * Tests that native-image generation doesn't run into an issue (like NPE) if the application * uses a java.security.Provider.Service which isn't part of the services shipped in the JDK. @@ -80,6 +99,10 @@ public void duringSetup(final DuringSetupAccess access) { */ @Test public void testUnknownSecurityServices() throws Exception { + if (FutureDefaultsOptions.isJDKInitializedAtRunTime()) { + /* Register the provider at run time. */ + Security.addProvider(new NoOpProvider()); + } final Provider registered = Security.getProvider("no-op-provider"); Assert.assertNotNull("Provider is not registered", registered); final Object impl = registered.getService("NoOp", "no-op-algo").newInstance(null); @@ -90,6 +113,10 @@ public void testUnknownSecurityServices() throws Exception { @Test public void testAutomaticSecurityServiceRegistration() { try { + if (FutureDefaultsOptions.isJDKInitializedAtRunTime()) { + /* Register the provider at run time. */ + Security.addProvider(new NoOpProviderTwo()); + } JCACompliantNoOpService service = JCACompliantNoOpService.getInstance("no-op-algo-two"); Assert.assertNotNull("No service instance was created", service); MatcherAssert.assertThat("Unexpected service implementation class", service, CoreMatchers.instanceOf(JcaCompliantNoOpServiceImpl.class));