Skip to content

Avoid race condition in ConcurrentReferenceHashMap #31008

Closed
@davmac314

Description

@davmac314

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.

Metadata

Metadata

Assignees

Labels

in: coreIssues in core modules (aop, beans, core, context, expression)type: enhancementA general enhancement

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions