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

8260274: Cipher.init(int, key) does not use highest priority provider for random bytes #3018

Closed
wants to merge 3 commits into from

Conversation

valeriepeng
Copy link

@valeriepeng valeriepeng commented Mar 15, 2021

Can someone help review this somewhat trivial change?

Updated JCAUtil class to return the cached SecureRandom object only when the provider configuration has not changed.
Added a regression test to check the affected classes, i.e. AlgorithmParameterGenerator, KeyPairGenerator, Cipher, KeyAgreement, KeyGenerator.

Thanks,
Valerie


Progress

  • Change must not contain extraneous whitespace
  • Commit message must refer to an issue
  • Change must be properly reviewed

Issue

  • JDK-8260274: Cipher.init(int, key) does not use highest priority provider for random bytes

Reviewers

Download

To checkout this PR locally:
$ git fetch https://git.openjdk.java.net/jdk pull/3018/head:pull/3018
$ git checkout pull/3018

To update a local copy of the PR:
$ git checkout pull/3018
$ git pull https://git.openjdk.java.net/jdk pull/3018/head

@bridgekeeper
Copy link

bridgekeeper bot commented Mar 15, 2021

👋 Welcome back valeriep! A progress list of the required criteria for merging this PR into master will be added to the body of your pull request. There are additional pull request commands available for use with this pull request.

@openjdk openjdk bot added the rfr Pull request is ready for review label Mar 15, 2021
@openjdk
Copy link

openjdk bot commented Mar 15, 2021

@valeriepeng The following label will be automatically applied to this pull request:

  • security

When this pull request is ready to be reviewed, an "RFR" email will be sent to the corresponding mailing list. If you would like to change these labels, use the /label pull request command.

@openjdk openjdk bot added the security security-dev@openjdk.org label Mar 15, 2021
@mlbridge
Copy link

mlbridge bot commented Mar 15, 2021

Webrevs

private static Provider[] cachedConfig = Security.getProviders();
private static SecureRandom instance = new SecureRandom();
public static SecureRandom instance(boolean checkConfig) {
synchronized (CachedSecureRandomHolder.class) {
Copy link
Member

Choose a reason for hiding this comment

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

Is there a performance regression because of this synchronization?

Copy link
Author

Choose a reason for hiding this comment

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

It depends, this may not be a "hot area" where there is a lot of contention? Or do you feel maybe we should just go with the slower "new SecureRandom()" call for each affected class? I am on the fence actually.

Copy link
Contributor

Choose a reason for hiding this comment

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

Since 5 classes have init() calls that would be acquiring this lock, something that could be independent would be better. Calling "new SecureRandom()" may or may not be better, but hope it wouldn't lock over operations during creation.

I'm just throwing out an idea:
Can the provider index number be verify in the list each time?

Better yet, maybe a way to have this triggered each time a provider is added or removed from the list?

Copy link
Member

Choose a reason for hiding this comment

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

Cipher and KeyPairGenerator could be used a lot in TLS context. The synchronization may impact in some circumstances. Supporting dynamic configuration could be error-prone. For example, one thread to set the providers, and another thread get the providers by calling Cipher.init(). Unless the set and get use the same lock, the return value of the get method is not predictable. The lock here is on CachedSecureRandomHolder for providers configuration getting, and no lock (or different lock) for providers configuration setting. So applications may be able to get the expected behaviors by introducing this synchronization.

As this behavior has been a while, I may not update the implementation. Instead, I would like to add an "implNote" tag to state that once the default provider is loaded, an implementation may be changed it any longer. Applications could use provider parameter if there is a preference.

If you want go ahead, maybe the performance impacted could be mitigated by locking less processes. For example:

if (checkConfig) {
Provider[] currConfig = Security.getProviders();
if (!Arrays.equals(cachedConfig, currConfig)) {
synchronized (CachedSecureRandomHolder.class) {
// double check the currConfig
currConfig = Security.getProviders();
if (!Arrays.equals(cachedConfig, currConfig)) {
instance = ...
cachedConfig = ...
}
}
}
}
return instance;

However, I still not very sure of the performance of Security.getProviders() because it is synchronized as well. Further, I may want to evaluate if the update of Security provider (for example Providers.setProviderList) could trigger an even to update the default instances.

Copy link
Contributor

Choose a reason for hiding this comment

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

I would definitely not call new SecureRandom() each time as that can have real performance problems (especially on systems which pull from /dev/random and may not have hardware entropy). The synchronization worries me too.

Could java.security.Security contain a "provider version" int (volatile, atomic, or similar) which is incremented every time the provider list is changed? Then this class (clearly using some non-public API) would check that value and if it is the same, just return the cached instance. If it has changed since the last time, then it would update instance and its remembered value of the index. I think that we might be able to do it without taking any locks in this class because even if the providers change out from under us, we returned a reasonable value (subject to multi-threading) and will fix things up when we're called the next time.

// using this and name collision to highlight what's a field and what's local
int providerListVersion = Security.nonPublicGetProviderListVersion();
if (this.providerListVersion != providerListVersion) {
   this.providerListVersion = providerListVersion;
   this.instance = new SecureRandom();
}
return this.instance;

Copy link
Contributor

Choose a reason for hiding this comment

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

Doesn't that still incur a JVM-wide lock whenever CachedSecureRandomHolder.getDefSecureRandom() is called? I recall a different JVM-wide lock in the getInstance path (JDK-7107615) creating significant performance issues.

Copy link
Member

Choose a reason for hiding this comment

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

I did not get the point that the code path should be unaffected". As there are a few update like:
init(keysize, JCAUtil.getSecureRandom());
init(keysize, JCAUtil.getDefSecureRandom());

I think there are some impact on the existing application which use the default sec

Or, I can also separate out the instance returned by getSecureRandom() so that code path should be unaffected, e.g.:

I did not get the point that the "code path should be unaffected"? JCAUtil.getDefSecureRandom() is used in updates like:
init(keysize, JCAUtil.getSecureRandom());
init(keysize, JCAUtil.getDefSecureRandom());

    // cached SecureRandom instance
    private static class CachedSecureRandomHolder {
        public static SecureRandom instance = new SecureRandom();
        private static Provider[] cachedConfig = Security.getProviders();
        private static SecureRandom def = instance;

        public static SecureRandom getDefault() {
            synchronized (CachedSecureRandomHolder.class) {
                Provider[] currConfig = Security.getProviders();
                if (!Arrays.equals(cachedConfig, currConfig)) {
                    def = new SecureRandom();
                    cachedConfig = currConfig;
                }
            }
            return def;
        }
    }

    /**
     * Get a SecureRandom instance. This method should be used by JDK
     * internal code in favor of calling "new SecureRandom()". That needs to
     * iterate through the provider table to find the default SecureRandom
     * implementation, which is fairly inefficient.
     */
    public static SecureRandom getSecureRandom() {
        return CachedSecureRandomHolder.instance;
    }

    /**
     * Get the default SecureRandom instance. This method is the
     * optimized version of "new SecureRandom()" which re-uses the default
     * SecureRandom impl if the provider table is the same.
     */
    public static SecureRandom getDefSecureRandom() {
        return CachedSecureRandomHolder.getDefault();
    }

Copy link
Author

Choose a reason for hiding this comment

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

As this behavior has been a while, I may not update the implementation. Instead, I would like to add an "implNote" tag to state that once the default provider is loaded, an implementation may be changed it any longer. Applications could use provider parameter if there is a preference.

Well, javadoc of the affected init methods clearly specifies that "get them using the SecureRandom implementation of the highest-priority installed provider as the source of randomness. ", thus I am not sure if we can just get around this with an @implNote.

Copy link
Author

Choose a reason for hiding this comment

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

I did not get the point that the code path should be unaffected". As there are a few update like:
init(keysize, JCAUtil.getSecureRandom());
init(keysize, JCAUtil.getDefSecureRandom());

What I mean is that the new suggestion will not impact callers of JCAUtil.getSecureRandom() which I thought what your performance regression means. To enforce that the most preferred SecureRandom impl is used, this for sure will incur some cost especially when the existing impl just returns a cached obj w/o any checking.

Copy link
Author

Choose a reason for hiding this comment

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

Could java.security.Security contain a "provider version" int (volatile, atomic, or similar) which is incremented every time the provider list is changed? Then this class (clearly using some non-public API) would check that value and if it is the same, just return the cached instance. If it has changed since the last time, then it would update instance and its remembered value of the index. I think that we might be able to do it without taking any locks in this class because even if the providers change out from under us, we returned a reasonable value (subject to multi-threading) and will fix things up when we're called the next time.

This and Tony's suggestion sounds similar and I can sure explore this.

@ascarpino
Copy link
Contributor

From a performance standpoint, having a single synchronized block on every init() operations is going to slow things down

@@ -26,7 +26,7 @@
package sun.security.jca;

import java.lang.ref.*;

import java.util.Arrays;
Copy link
Contributor

Choose a reason for hiding this comment

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

Import no longer needed.

Copy link
Author

Choose a reason for hiding this comment

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

Sure, will remove.

* optimized version of "new SecureRandom()" which re-uses the default
* SecureRandom impl if the provider table is the same.
*/
public static SecureRandom getDefSecureRandom() {
Copy link
Contributor

Choose a reason for hiding this comment

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

When should people use getSecureRandom rather than getDefSecureRandom()? Are we leaving the old to avoid changing behavior out from under people who may not expect it?

Copy link
Author

Choose a reason for hiding this comment

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

To minimize the impact, I leave the JCAUtil.getSecureRandom() impl as is. I suppose this is more for JDK internal code which is not required to use the most preferred SecureRandom impl. There are quite a few callers to this method and I feel it's better to leave them out of this change.

Copy link
Member

Choose a reason for hiding this comment

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

To minimize the impact, I leave the JCAUtil.getSecureRandom() impl as is. I suppose this is more for JDK internal code which is not required to use the most preferred SecureRandom impl. There are quite a few callers to this method and I feel it's better to leave them out of this change.

The internal use may be just to avoid to create new instances of SecureRandom. Those use may need to use the latest updated secure random provider as well, for example the use in DSA key pair generation (DSAKeyPairGenerator.java).

With this update, I think the performance impact should be minimal and we may be able to have a uniform behavior no matter internal or external uses.

Copy link
Author

Choose a reason for hiding this comment

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

Maybe. We can do that in a separate changeset...

Comment on lines 85 to 92
if (def == null) {
synchronized (JCAUtil.class) {
if (def == null) {
def = new SecureRandom();
}
}
}
return def;
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we need to worry about clearDefSecureRandom() being called between lines 88 and 92?

Thread 1: getDefSecureRandom()#85 (def is null)
Thread 1: getDefSecureRandom()#86
Thread 1: getDefSecureRandom()#87 (def is still null)
Thread 1: getDefSecureRandom()#88 (def is not null)
Thread 2: clearDefSecureRandom() (def is null again)
Thread 1: getDefSecureRandom()#92 (return def which is incorrectly null)

Suggested change
if (def == null) {
synchronized (JCAUtil.class) {
if (def == null) {
def = new SecureRandom();
}
}
}
return def;
SecureRandom result = def;
if (result == null) {
synchronized (JCAUtil.class) {
result = def;
if (result == null) {
def = result = new SecureRandom();
}
}
}
return result;

Copy link
Author

Choose a reason for hiding this comment

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

Sure, good point. I will update. Thanks for catching this.

Copy link
Contributor

@SalusaSecondus SalusaSecondus left a comment

Choose a reason for hiding this comment

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

Not that my review counts towards approval, but this looks good to me (and fixes some issues I've been wrestling with on and off for several years now, so I'm really happy to see this change).

public static SecureRandom getDefSecureRandom() {
SecureRandom result = def;
if (result == null) {
synchronized (JCAUtil.class) {
Copy link
Member

Choose a reason for hiding this comment

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

Could this lock be avoided if set the value in the Providers update (or when the providers list is updated)?

Copy link
Author

Choose a reason for hiding this comment

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

Well, then we have to pay the cost of "new SecureRandom()" at every provider list update when it may not be needed. It's hard to say which way is better... I thought about it and it seems more reasonable to only pay the cost when the SecureRandom object is needed.

Copy link
Member

Choose a reason for hiding this comment

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

It makes sense to me.

@ascarpino
Copy link
Contributor

I'm fine with the change. The main code path should perform as it did previous to the change and I don't think the lock here is a big deal as it will only be triggered during a provider change. One small delay in a very rare case should be fine

@openjdk
Copy link

openjdk bot commented Mar 17, 2021

@valeriepeng This change now passes all automated pre-integration checks.

ℹ️ This project also has non-automated pre-integration requirements. Please see the file CONTRIBUTING.md for details.

After integration, the commit message for the final commit will be:

8260274: Cipher.init(int, key) does not use highest priority provider for random bytes

Reviewed-by: ascarpino, xuelei

You can use pull request commands such as /summary, /contributor and /issue to adjust it as needed.

At the time when this comment was updated there had been 82 new commits pushed to the master branch:

  • 9225a23: 8263108: Class initialization deadlock in java.lang.constant
  • 5d5813a: 8263757: Remove serviceability/sa/ClhsdClasses.java from ZGC problem list
  • 50ff0d4: 8263756: Fix ZGC ProblemList entry for serviceability/sa/ClhsdbSymbol.java
  • 99b39aa: 8262807: Note assumptions of core reflection modeling and parameter handling
  • 26234b5: 8254979: Class.getSimpleName() returns non-empty for lambda and method
  • 83a49ef: 8263753: two new tests from JDK-8261671 fail with "Error. can not find ClassFileInstaller in test directory or libraries"
  • 24afa36: 8263726: divideToIntegralValue typo on BigDecimal documentation
  • cdf78e4: 8262298: G1BarrierSetC2::step_over_gc_barrier fails with assert "bad barrier shape"
  • 7674da4: 8262398: Shenandoah: Disable nmethod barrier and stack watermark when running with passive mode
  • 4f4ca0e: 8261671: X86 I2L conversion can be skipped for certain masked positive values
  • ... and 72 more: https://git.openjdk.java.net/jdk/compare/b2f7c58dc9f29278a12070128a6eff75bbb12149...master

As there are no conflicts, your changes will automatically be rebased on top of these commits when integrating. If you prefer to avoid this automatic rebasing, please check the documentation for the /integrate command for further details.

➡️ To integrate this PR with the above commit message to the master branch, type /integrate in a new comment.

@openjdk openjdk bot added the ready Pull request is ready to be integrated label Mar 17, 2021
@valeriepeng
Copy link
Author

Thanks all for the review and feedback~

@valeriepeng
Copy link
Author

/integrate

@openjdk openjdk bot closed this Mar 18, 2021
@openjdk openjdk bot added integrated Pull request has been integrated and removed ready Pull request is ready to be integrated rfr Pull request is ready for review labels Mar 18, 2021
@openjdk
Copy link

openjdk bot commented Mar 18, 2021

@valeriepeng Since your change was applied there have been 103 commits pushed to the master branch:

  • 6aa28b3: 8263827: Suspend "missing" javadoc doclint checks for smartcardio
  • ed1e25d: 8263833: Stop disabling warnings for sunFont.c with gcc
  • 788e30c: 8263320: [test] Add Object Stream Formatter to work with test utility HexPrinter
  • fa0f161: 8263742: (bf) MappedByteBuffer.force() should use the capacity as its upper bound
  • c82a673: 8262001: java/lang/management/ThreadMXBean/ResetPeakThreadCount.java failed with "RuntimeException: Current Peak = 14 Expected to be == previous peak = 7 + 8"
  • 01ddf3d: 8263622: The java.awt.color.ICC_Profile#setData invert the order of bytes for the "head" tag
  • e34f766: 8252723: Run stack016.java also with C2-only
  • 2173fed: 8263439: getSupportedAttributeValues() throws NPE for Finishings attribute
  • e543a50: 8261352: Create implementation for component peer for all the components who should be ignored in a11y interactions
  • 21db0f6: 8263659: Reflow GTestResultParser for better readability
  • ... and 93 more: https://git.openjdk.java.net/jdk/compare/b2f7c58dc9f29278a12070128a6eff75bbb12149...master

Your commit was automatically rebased without conflicts.

Pushed as commit 434a399.

💡 You may see a message that your pull request was closed with unmerged commits. This can be safely ignored.

@valeriepeng valeriepeng deleted the JDK-8260274 branch March 18, 2021 23:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
integrated Pull request has been integrated security security-dev@openjdk.org
4 participants