Description
Affects: 6.0.11 (current HEAD) and probably earlier versions
The ConcurrentReferenceHashMap
used by Spring has a race condition which can cause it to incorrectly return null for a get operation where they key is in the map.
In (inner class) Segment.getReference(...)
, line 500 (method starts at line 493), we see:
// Use a local copy to protect against other threads writing
Reference<K, V>[] references = this.references;
int index = getIndex(hash, references);
Reference<K, V> head = references[index];
return findInChain(head, key, hash);
(This is called from ConcurrentWeakHashMap.getEntry(...)
, itself called by various methods including get(...)). At this point no lock is held. The comment is the clue: it takes a local reference to the existing references
array (this.references
) in order to protect against issues caused by other threads re-assigning this field.
Taking a local reference however doesn't protect against other threads writing to the same array. That is exactly what can happen, in certain (rare) circumstances, within the Segment.restructure(...)
method (line 605+ below, method starts at line 580):
// Either create a new table or reuse the existing one
Entry<K, V>[] restructured =
(resizing ? createEntryArray(restructureSize) : this.entries); // <=== same array
// Restructure
for (int i = 0; i < this.entries.length; i++) {
entry = this.entries[i];
if (!resizing) {
restructured[i] = null; // <=== write HERE!
Notice that the restructure uses the original array if it is not resizing (line marked with "same array" comment) and that it temporarily assigns each entry in turn with null ("write HERE!"). The entry is restored later on in the loop, but this moment when it is null is the problem, since it may cause Segment.getReference(...)
(first method posted above), if being executed concurrently in another thread, to retrieve the null value when looking up the entry:
Reference<K, V> head = references[index]; // <=== write affects this read!
This causes it to return null, as if the map does not contain an entry with the given key, when it fact it may do so.
In practice the issue may be very difficult to reproduce and I'm sure it is uncommon "in the wild" but I was able to reproduce the issue by inserting some sleep calls at key points within the ConcurrentReferenceHashMap code and executing operations in parallel from two different threads, while also ensuring that a key in the map became garbage collected before the call to the 2nd operation so that it would perform a restructure
after the first (get) operation had taken its reference to this.references
but before it retrieved the entry. This was based on an already slightly modified version of the class which we are using internally but I have inspected the version in Spring carefully and I'm fairly sure the same issue is present.