Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[CRYPTO-169][FOLLOWUP] Fix reentrancy of CryptoRandomFactory#getCryptoRandom #259

Merged
merged 7 commits into from
Oct 23, 2023

Conversation

LuciferYang
Copy link
Contributor

@LuciferYang LuciferYang commented Oct 22, 2023

There are some issues with the reentrancy of CryptoRandomFactory.getCryptoRandom(properties) method.

For the following test case:

@Test
public void testReentrancyOfExceptionInInitializerErrorRandom() throws GeneralSecurityException, IOException {
    final Properties properties = new Properties();
    String classes = ExceptionInInitializerErrorRandom.class.getName().concat(",")
            .concat(CryptoRandomFactory.RandomProvider.JAVA.getClassName());
    properties.setProperty(CryptoRandomFactory.CLASSES_KEY, classes);
    for (int i = 0; i < 3; i++) {
        try (final CryptoRandom random = CryptoRandomFactory.getCryptoRandom(properties)) {
            assertEquals(JavaCryptoRandom.class.getName(), random.getClass().getName());
         }
    }
} 

The second call to CryptoRandomFactory.getCryptoRandom(properties) will result in the following error:

java.lang.NoClassDefFoundError: Could not initialize class org.apache.commons.crypto.random.ExceptionInInitializerErrorRandom
	at java.base/java.lang.Class.forName0(Native Method)
	at java.base/java.lang.Class.forName(Class.java:534)
	at java.base/java.lang.Class.forName(Class.java:513)
	at org.apache.commons.crypto.utils.ReflectionUtils.getClassByNameOrNull(ReflectionUtils.java:93)
	at org.apache.commons.crypto.utils.ReflectionUtils.getClassByName(ReflectionUtils.java:64)
	at org.apache.commons.crypto.random.CryptoRandomFactory.getCryptoRandom(CryptoRandomFactory.java:189)
	at org.apache.commons.crypto.random.CryptoRandomFactoryTest.testReentrancyOfExceptionInInitializerErrorRandom(CryptoRandomFactoryTest.java:99)
	at java.base/java.lang.reflect.Method.invoke(Method.java:580)
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
Caused by: java.lang.ExceptionInInitializerError: Exception java.lang.IllegalStateException: java.security.GeneralSecurityException: ExceptionInInitializerErrorRandom init failed [in thread "main"]
	at org.apache.commons.crypto.random.ExceptionInInitializerErrorRandom.<clinit>(ExceptionInInitializerErrorRandom.java:32)
	at java.base/java.lang.Class.forName0(Native Method)
	at java.base/java.lang.Class.forName(Class.java:534)
	at java.base/java.lang.Class.forName(Class.java:513)
	at org.apache.commons.crypto.utils.ReflectionUtils.getClassByNameOrNull(ReflectionUtils.java:93)
	at org.apache.commons.crypto.utils.ReflectionUtils.getClassByName(ReflectionUtils.java:64)
	at org.apache.commons.crypto.random.CryptoRandomFactory.getCryptoRandom(CryptoRandomFactory.java:189)
	at org.apache.commons.crypto.random.CryptoRandomFactoryTest.testReentrancyOfExceptionInInitializerErrorRandom(CryptoRandomFactoryTest.java:96)
	... 3 more

This is due to the Class.forName method. If the first initialization fails, subsequent identical calls will not attempt to initialize the class again, but will throw NoClassDefFoundError.

So this PR changes to add a collection in ReflectionUtils to record the class names that failed to initialize (recorded when Class.forName first fails to initialize), to ensure that known failed initialization classes will not be attempted to load repeatedly.

This PR removes the modification of the CryptoRandomFactory#getCryptoRandom method in #258, because under the above changes, CryptoRandomFactory#getCryptoRandom will no longer catch ExceptionInInitializerError.

@LuciferYang
Copy link
Contributor Author

LuciferYang commented Oct 22, 2023

It seems that there are some differences between Java 8 and 17, let me take another look.

@garydgregory
Copy link
Member

It seems that there are some differences between Java 8 and 17, let me take another look.

🆗

@codecov-commenter
Copy link

codecov-commenter commented Oct 22, 2023

Codecov Report

Merging #259 (fa434ec) into master (a164574) will increase coverage by 0.16%.
The diff coverage is 100.00%.

@@             Coverage Diff              @@
##             master     #259      +/-   ##
============================================
+ Coverage     74.66%   74.83%   +0.16%     
- Complexity      441      444       +3     
============================================
  Files            38       38              
  Lines          1816     1820       +4     
  Branches        177      178       +1     
============================================
+ Hits           1356     1362       +6     
+ Misses          349      348       -1     
+ Partials        111      110       -1     
Files Coverage Δ
...che/commons/crypto/random/CryptoRandomFactory.java 93.18% <ø> (+3.18%) ⬆️
...g/apache/commons/crypto/utils/ReflectionUtils.java 93.47% <100.00%> (+1.81%) ⬆️

📣 We’re building smart automated test selection to slash your CI/CD build times. Learn more

@@ -206,6 +206,18 @@ public static CryptoRandom getCryptoRandom(final Properties props)
} else {
throw initializerError;
}
} catch (final NoClassDefFoundError noClassDefFoundError) {
Throwable initializerError = noClassDefFoundError.getCause();
Copy link
Contributor Author

@LuciferYang LuciferYang Oct 22, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are some differences here between Java 8/11 and Java 17/21:

  • Java 8/11: Will catch NoClassDefFoundError, but NoClassDefFoundError.getCause is null.
  • Java 17/21: Will catch NoClassDefFoundError, and NoClassDefFoundError.getCause is ExceptionInInitializerError, but ExceptionInInitializerError.getCause is null

So the check way here is a bit tricky, do you have any better suggestions? @garydgregory

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@LuciferYang
Not off the top of my head...


if (set.contains(name)) {
return null;
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a collection to record classes that have ExceptionInInitializerError to avoid unnecessary reentry of the Class.forName method in this scenario. This collection cannot be garbage collected, otherwise there will still be reentry issues.

@@ -63,6 +68,9 @@ private static abstract class AbstractNegativeCacheSentinel {
public static Class<?> getClassByName(final String name) throws ClassNotFoundException {
final Class<?> ret = getClassByNameOrNull(name);
if (ret == null) {
if (INIT_ERROR_CLASSES.get(CLASSLOADER).contains(name)) {
throw new IllegalStateException("Class " + name + " initialization error");
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An additional judgment has been added to distinguish ClassNotFoundException. Here an IllegalStateException is manually thrown, there may be a more appropriate exception type.

@@ -198,14 +198,6 @@ public static CryptoRandom getCryptoRandom(final Properties props)
} catch (final Exception e) {
lastException = e;
errorMessage.append("CryptoRandom: [" + className + "] failed with " + e.getMessage());
} catch (final ExceptionInInitializerError initializerError) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ExceptionInInitializerError is uniformly handled in ReflectionUtils, and an exception is thrown at the same time. So, there is no need to handle it here.

@garydgregory garydgregory merged commit f0c92d5 into apache:master Oct 23, 2023
16 checks passed
@LuciferYang
Copy link
Contributor Author

Thanks @garydgregory ~

@garydgregory
Copy link
Member

Thanks @garydgregory ~

YW!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants