-
Notifications
You must be signed in to change notification settings - Fork 5.8k
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
8345139: Fix bugs and inconsistencies in the Provider services map #22613
base: master
Are you sure you want to change the base?
Conversation
Co-authored-by: Francisco Ferrari Bihurriet <fferrari@redhat.com> Co-authored-by: Martin Balao <mbalao@redhat.com>
/contributor add fferrari |
/contributor add mbalao |
👋 Welcome back fferrari! A progress list of the required criteria for merging this PR into |
❗ This change is not yet ready to be integrated. |
@franferrax |
@franferrax |
@franferrax The following label will be automatically applied to this pull request:
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. |
Webrevs
|
/reviewers 2 Reviewer |
This is a large change so I think it should require at least 2 Reviewer approvals. |
@seanjmullan |
/* | ||
* Enum to inform the result of an operation on the services map. | ||
*/ | ||
enum SvcOpResult { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why use an enum here when a boolean is sufficient?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just to add some more semantic value but we don't have a strong opinion, we can replace it with boolean if you want.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, please do that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
Co-authored-by: Martin Balao Alonso <mbalao@redhat.com> Co-authored-by: Francisco Ferrari Bihurriet <fferrari@redhat.com>
@franferrax This pull request has been inactive for more than 4 weeks and will be automatically closed if another 4 weeks passes without any activity. To avoid this, simply add a new comment to the pull request. Feel free to ask for assistance if you need help with progressing this pull request towards integration! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you explain the need for Current
and Legacy
interfaces? You have calls to doLegacy()
for adding and removing entries, but I do not see why this is necessary since both APIs are in ServiceMapImpl
.
/* | ||
* Enum to inform the result of an operation on the services map. | ||
*/ | ||
enum SvcOpResult { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, please do that.
*/ | ||
private Service(Provider provider, ServiceKey algKey) { | ||
assert algKey.algorithm.intern() == algKey.algorithm : | ||
"Algorithm should be interned."; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is intern()
a requirement for this constructor?
Following the call stack this AssertionError is thrown with Provider.load()
and Provider.putAll()
at a minimum. This could change behavior and I think it should be removed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These assertions are internal invariants that we want to sanity-check and document, specially for future code changes. They will never occur and do not depend on user API input. We went through all of them again to make sure that they are correct. For example, ServiceKey
instances received in the referred Service
constructor parameter must always have an internalized algorithm (the original algorithm string converted to uppercase). If you know of a call stack which may allow non-internalized algKey.algorithm
, please let us know because it is a bug —we couldn't find any.
In the Security Manager days, the assumption was that Providers had enough privileges already to be trusted with internalizing Strings. On the contrary, code invoking getService
was not necessarily trusted enough to internalize Strings used for queries. While this is not longer the case, we kept the distinction because Providers will use the Strings indeed and is a reduced set, whereas queries may be more open bounded.
"the algorithm."; | ||
assert aliasKey.type.equals(type) : "Invalid alias key type."; | ||
assert aliasKey.algorithm.intern() == aliasKey.algorithm : | ||
"Alias should be interned."; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All these asserts look like they leak into the public API. If something does not match your requirements, then log a detailed message using debug
and do not add the entry.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please have a look at my comment above.
@franferrax this pull request can not be integrated into git checkout JDK-8345139
git fetch https://git.openjdk.org/jdk.git master
git merge FETCH_HEAD
# resolve conflicts and follow the instructions given by git merge
git commit -m "Merge master"
git push |
Co-authored-by: Martin Balao Alonso <mbalao@redhat.com> Co-authored-by: Francisco Ferrari Bihurriet <fferrari@redhat.com>
Fix conflict caused by e20bd01: 1. Ignore the change, as this had already been identified and fixed (see JDK-8345139, section 3.1). 2. Remove the test, as it is already covered by ServicesConsistency::testInvalidServiceNotReturned. 3. Add the corresponding bug ID to ServicesConsistency. Co-authored-by: Francisco Ferrari Bihurriet <fferrari@redhat.com> Co-authored-by: Martin Balao Alonso <mbalao@redhat.com>
The reason for having |
NOTE: as explained in the 35e2eae merge commit message, this PR now deletes jdk/test/jdk/java/security/Provider/ServicesConsistency.java Lines 835 to 839 in 35e2eae
Just in case, we made sure that the deleted |
Constructors assign the fields in the same order.
classCache = null; | ||
constructorCache = null; | ||
hasKeyAttributes = null; | ||
supportedFormats = null; | ||
supportedClasses = null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are these necessary? The other constructor didn't set them.
} else { | ||
attributes = new HashMap<>(svc.attributes); | ||
} | ||
registered = false; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I didn't see it's set to true
in any of the constructors; also the default value is already false
, why only explicitly set it to false
here?
void removeAttribute(String type, String value) { | ||
if (attributes.isEmpty()) { | ||
return; | ||
} | ||
if (value == null) { | ||
attributes.remove(new UString(type)); | ||
} else { | ||
attributes.remove(new UString(type), value); | ||
} | ||
private void removeAttribute(String attrName, String attrValue) { | ||
UString attrKey = new UString(attrName); | ||
assert attributes.get(attrKey) == attrValue : | ||
"Attribute value expected to exist with the same identity."; | ||
attributes.remove(attrKey, attrValue); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is the new impl assuming attrValue
should never be null
? Based on javadoc of Map.remove(Object key, Object value)
, the new impl removes the entry when the associated value is null
vs the original impl removes the entry regardless of the associated value.
this.className = className; | ||
if (aliases == null) { | ||
this.aliases = Collections.emptyList(); | ||
} else { | ||
this.aliases = new ArrayList<>(aliases); | ||
} | ||
aliasKeys = Collections.emptyMap(); | ||
if (attributes == null) { | ||
this.attributes = Collections.emptyMap(); | ||
} else { | ||
this.attributes = new HashMap<>(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Initialize with attributes.size()
and load factor 1.0 if both are the same size?
// For services added to a ServicesMap, this is a map from alias service | ||
// keys to alias string values. Empty map if no aliases. While map | ||
// entries derive from the aliases field, keys are not repeated | ||
// (case-insensitive comparison) and not equal to the algorithm. For | ||
// services (still) not added to a ServicesMap, value is an empty map. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we re-write it to summarize the conditions for empty map? It could be easier to read/understand.
For example: empty map if no aliases or if this service is not yet added to a ServiceMap
.
The part of case-insensitive comparision, it's due to the impl of ServiceKey
, right? Maybe we can simply refer to that no need to describe it here.
// added to this map. | ||
private final Map<ServiceKey, Map<UString, String>> serviceAttrProps; | ||
|
||
ServicesMap() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: add comment for this constructor?
@@ -635,7 +634,7 @@ public synchronized Object merge(Object key, Object value, | |||
|
|||
// let javadoc show doc from superclass | |||
@Override | |||
public Object get(Object key) { | |||
public synchronized Object get(Object key) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about the getProperty(String) method on line 675? Add @Override
tag and synchronized
keyword there also? And the keySet()
and values()
methods on line 432 and 444 respectively? What is the criteria for synchronizing the method of the Provider
class?
* values. | ||
*/ | ||
private record MappingInfo(Service svc, ServiceKey algKey, | ||
Boolean isLegacy) {} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is comment states that isLegacy
may be null, but then I also see a few if-cond using isLegacy
directly like its a boolean, wouldn't it lead to NPE if isLegacy
is null
?
} else { | ||
// The service was added with the Current API. Overwrite | ||
// the alias entry on the services map without modifying | ||
// the service that is currently using it. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is the "service" in the above line really means the provider service
entry? If so, may be "associated with" is better than "using". Also there is no code under this comment block, where is the action of "overwrite the alias entry on the services map"?
return false; | ||
} | ||
|
||
if (mi.isLegacy) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For legacy entry, there is no check on the legacyApiCall
value, is this due to the call path from resolveKeyConflict
method? However, should a legacy entry be removed by the removeService
method? If not, then additional check may be needed?
String canonicalPropKey = propKey; | ||
if (oldMi.svc != null) { | ||
// The service exists. Get its Properties map entry. Note: | ||
// Services added through an alias or an attribute may don't |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"may don't"? Maybe you mean "may not" or simply "don't"
/* | ||
* This method tries to find a service on the map (based on an | ||
* algorithm or alias) and pass a copy of it to an update callback | ||
* (copy-on-write). If the service found was added with the Current API, | ||
* no update should be done. If a service was not found, a new instance | ||
* may be created. | ||
* | ||
* The updated version of the service is put on the services map. | ||
* Algorithm and alias based entries pointing to the old version of the | ||
* service are overwritten. | ||
*/ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Comment for this method should note that this method is only for the Legacy registration...
private boolean updateSvc(ServiceKey key, | ||
ServiceUpdateCallback updateCb) { | ||
Service newSvc; | ||
MappingInfo oldMi = find(key); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oldMi
could just simply be mi
? There is no newMi
in the same method anywhere.
/* | ||
* Enum to determine if changes to the Properties map must be applied by the | ||
* caller (UPDATE) or skipped (SKIP). | ||
* | ||
* If a change does not concern a ServicesMap, UPDATE is returned. An | ||
* example of this is when adding, modifying or removing an entry that is | ||
* not a service, alias or attribute. | ||
* | ||
* If the change concerns a ServicesMap, SKIP is returned. The change may | ||
* have been applied internally or ignored due to an error. In the former | ||
* case, Properties map entries are synchronized. In the latter, Properties | ||
* map entries are not modified. | ||
*/ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not too sure I get this comment block. Judging from the impl, this enum seems to be used to indicate whether the Properties map is updated. The part about ServiceMap is especially confusing. Is this enum for Properties map or ServicesMap? In addition, instead of stating "an entry that is not a service, alias or attribute.", can we just state the remaining types? Is "In the former case" refer to the UPDATE? In that paragraph. there is only one case. Lastly, there is only 2 values to this enum, can't this be replaced with a boolean?
} else if (value != null && oldValue instanceof String oldValueS && | ||
opType == OPType.ADD) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Which method is this else-block for? value
is not null and not instanceof String
and oldValue
is instanceof String
and opType
is ADD?
// From the ServicesMap point of view, this could be equivalent | ||
// to a removal. In any case, let the caller proceed with the | ||
// Properties map update. | ||
parseLegacy(servicesMap, ks, oldValueS, OPType.REMOVE); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
no return here and falls through to the line 1512? Is this really intended?
} | ||
return o; | ||
private Object implRemove(Object key) { | ||
Object oldValue = super.get(key); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oldValue
isn't really old value, is it? Naming it oldValue
and pass it to doLegacyOp()
as value
is very confusing.
Hi, this pull request implements the fixes for bugs and inconsistencies described in JDK-8345139.
New services map design
Here is the high-level hierarchy of the new services map design:
servicesMap
(ServicesMap
)services
(Map<ServiceKey, Service>
): unifies the previousserviceMap
andlegacyMap
legacySvcKeys
(Set<ServiceKey>
): set indicating which keys inservices
correspond to the Legacy APIserviceProps
(Map<ServiceKey, String>
): keeps track of the provider Hashtable entries that originated services entries (1)serviceAttrProps
(Map<ServiceKey, Map<UString, String>>
): keeps track of the provider Hashtable entries that originated service attributes (1)serviceSet
(AtomicReference<Set<Service>>
): part of a lock-free mechanism to implement a consistent version of thegetServices()
readers methodboolean putService(Service svc)
boolean removeService(Service svc)
boolean putClassNameLegacy(ServiceKey key, String className, String propKey)
boolean putAliasLegacy(ServiceKey key, ServiceKey aliasKey, String propKey)
boolean putAttributeLegacy(ServiceKey key, String attrName, String attrValue, String propKey)
boolean removeLegacy(ServiceKey key, String className)
boolean removeAliasLegacy(ServiceKey key, ServiceKey aliasKey)
boolean removeAttributeLegacy(ServiceKey key, String attrName, String attrValue)
GetInstance
APIs)Set<Service> getServices()
Service getService(ServiceKey key)
void clear()
(1): to maintain the consistency between the provider's
servicesMap
and its Hashtable entries, even if Legacy API updates occur through properties with different casing, or aliases instead of main algorithms.Testing
As part of our testing, we observed all the tests pass in the following categories:
jdk:tier1
(see GitHub Actions run)jdk/com/sun/crypto
jdk/java/security
jdk/java/security/Provider/ServicesConsistency.java
Additionally, we found no regressions with respect to this pull request baseline (bf0debc) in the following category:
jdk/sun/security
Same results in both codebases
New
ServicesConsistency.java
testThe new
ServicesConsistency
test checks several of the cases described in the JBS issue, plus other similar variants. Here is a mapping between some of the test cases and their corresponding JBS issue section (using text fragments links), known to describe the tested problem.ServicesConsistency::testInvalidGetServicesRemoval
ServicesConsistency::testInvalidGetServiceRemoval
ServicesConsistency::testPutAllIsAtomic
ServicesConsistency::testReplaceAllIsAtomic
ServicesConsistency::testInvalidCachedClass
ServicesConsistency::testInvalidCachedHasKeyAttributes
ServicesConsistency::testInvalidCachedSupportedKeyFormats
ServicesConsistency::testSerializationClassnameConsistency
ServicesConsistency::testCurrentAPIServicesOverride
ServicesConsistency::testLegacyAPIServicesOverride
ServicesConsistency::testLegacyAPIAliasCannotBeAlgorithm
ServicesConsistency::testInvalidServiceNotReturned
ServicesConsistency::testComputeDoesNotThrowNPE
ServicesConsistency::testMergeDoesNotThrowNPE
This contribution is co-authored by @franferrax and @martinuy.
Progress
Issue
Contributors
<fferrari@openjdk.org>
<mbalao@openjdk.org>
Reviewing
Using
git
Checkout this PR locally:
$ git fetch https://git.openjdk.org/jdk.git pull/22613/head:pull/22613
$ git checkout pull/22613
Update a local copy of the PR:
$ git checkout pull/22613
$ git pull https://git.openjdk.org/jdk.git pull/22613/head
Using Skara CLI tools
Checkout this PR locally:
$ git pr checkout 22613
View PR using the GUI difftool:
$ git pr show -t 22613
Using diff file
Download this PR as a diff file:
https://git.openjdk.org/jdk/pull/22613.diff
Using Webrev
Link to Webrev Comment