Skip to content

Fix StackOverflowError with AbstractList after using mockSingleton#3790

Merged
raphw merged 2 commits intomockito:mainfrom
jselbo:fix-singleton-mock-recursion
Mar 4, 2026
Merged

Fix StackOverflowError with AbstractList after using mockSingleton#3790
raphw merged 2 commits intomockito:mainfrom
jselbo:fix-singleton-mock-recursion

Conversation

@jselbo
Copy link
Copy Markdown
Collaborator

@jselbo jselbo commented Feb 26, 2026

(I noticed this issue when integrating with Mockito-Kotlin and running its test suite)

WeakHashMap.get() calls hashCode() on its keys, which for instrumented classes triggers the byte buddy advice, calling isMocked() -> getSingletonMockInterceptor() -> WeakHashMap.get() recursively.

This adds a thread-local recursion guard around the WeakHashMap.get() call.

Alternatively, we could use a different map implementation like IdentityHashMap which doesn't call hashCode() but I didn't want to diverge from mockStatic which also uses a WeakHashMap (though I'm not convinced the weak semantics are important)

@jselbo jselbo force-pushed the fix-singleton-mock-recursion branch from cd85319 to 91b6035 Compare February 26, 2026 23:22
@codecov-commenter
Copy link
Copy Markdown

codecov-commenter commented Feb 27, 2026

Codecov Report

❌ Patch coverage is 77.63158% with 17 lines in your changes missing coverage. Please review.
✅ Project coverage is 86.83%. Comparing base (25f1395) to head (aed02a1).
⚠️ Report is 6 commits behind head on main.

Files with missing lines Patch % Lines
...ito/internal/util/collections/WeakIdentityMap.java 77.33% 4 Missing and 13 partials ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##               main    #3790      +/-   ##
============================================
+ Coverage     86.50%   86.83%   +0.33%     
- Complexity     3004     3012       +8     
============================================
  Files           343      341       -2     
  Lines          9099     9095       -4     
  Branches       1121     1124       +3     
============================================
+ Hits           7871     7898      +27     
+ Misses          945      908      -37     
- Partials        283      289       +6     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Copy Markdown
Member

@raphw raphw left a comment

Choose a reason for hiding this comment

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

This is a good guardrail. Could I asked you to add the same mechanism in mockedStatics? It might call a hashCode that delegates to a static utility, what would cause the same issue, so we should handle this consistently.

@jselbo
Copy link
Copy Markdown
Collaborator Author

jselbo commented Feb 27, 2026

Actually I think it's not susceptible to the same issue because for isMockedStatic we pass a Class type to WeakHashMap.containsKey() which can't be instrumented.

@jselbo jselbo requested a review from raphw February 27, 2026 16:48
@raphw
Copy link
Copy Markdown
Member

raphw commented Feb 27, 2026

Indeed. Would need a custom hashCode method in a future JVM that calles a static method outside of Class, what sounds unlikely. I am however wondering why hashCode is called at all. I would expect identity maps since hashcodes and equality can be broken on user objects. Let me have a quick look.

@raphw
Copy link
Copy Markdown
Member

raphw commented Feb 27, 2026

Ah, I think we should avoid the weak hash map here: https://github.com/mockito/mockito/blob/main/mockito-core/src/main/java/org/mockito/internal/creation/bytebuddy/InlineDelegateByteBuddyMockMaker.java#L698

If the hashCode/equals method is broken this will yield weird results, and that even more if two equal singleton objects exist. I would go for an IndentityHashMap<MockitoWeakReference<Object>> and use identity comparison on the (pseudo) MockitoWeakReference. For bucketing, use system hash code.

@raphw
Copy link
Copy Markdown
Member

raphw commented Feb 27, 2026

With this, there should never be calls on the object itself and the guardrail is no longer needed.

Copy link
Copy Markdown
Member

@raphw raphw left a comment

Choose a reason for hiding this comment

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

Avoid use of methods on singleton objects and use identity reference.

@jselbo
Copy link
Copy Markdown
Collaborator Author

jselbo commented Feb 27, 2026

I am wondering if the weak reference semantics are still important for this impl. IdentityHashMap<MockWeakReference<Object>, ..> is not exactly the same as WeakHashMap, and I think we'd need extra care/complexity to actually achieve weak ref behavior in the same way.

I think for "singletons" the weak ref behavior doesn't really matter. For Java enums at least, they should never be GCed.

@jselbo jselbo force-pushed the fix-singleton-mock-recursion branch from 91b6035 to 432b2f2 Compare February 27, 2026 20:25
@jselbo jselbo requested a review from raphw February 27, 2026 21:20
Copy link
Copy Markdown
Member

@raphw raphw left a comment

Choose a reason for hiding this comment

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

I think the weak reference is relevant here. Not often, but in some cases it can drag an entire class loader with it which would otherwise be collected, causing memory leaks in case that a single test does not properly close the mocking context.

In order to implement a proper weak map, you need to register s reference queue and check for new items on it. Those entries can then be removed from the map. Otherwise, you need to keep the system hash code stored in the custom reference and compare by identity. If a weak reference is null it cannot be equal any more as it's been collected.

@jselbo
Copy link
Copy Markdown
Collaborator Author

jselbo commented Mar 2, 2026

I'm wary of building a new map implementation from scratch to satisfy both weakness and identity. I found some discussion on https://stackoverflow.com/questions/22910375/combo-of-identityhashmap-and-weakhashmap which points to Guava or https://github.com/plume-lib/hashmap-util/blob/main/src/main/java/org/plumelib/util/WeakIdentityHashMap.java (unclear how battle-tested this is). I guess neither are really viable options,.

It seems to me a recursion guard (like the original commit) is simpler to maintain even though it's theoretically incomplete. What do you think @raphw ?

@raphw
Copy link
Copy Markdown
Member

raphw commented Mar 2, 2026

I think we need to support identity and weak references, we cannot choose as lacking either will lead to subtle bugs, and with Mockito's reach users will run into those. In our case, I would like to avoid the dependency, as Guava can quickly lead to conflicts. Wrapping the mocks seems like a realistic goal as we do not expose the map as general purpose. I believe that is the solution we should aim for.

Replace WeakHashMap with a WeakIdentityMap that uses
System.identityHashCode and == for key comparison. This avoids calling
instrumented hashCode()/equals() methods on mock instances during map
lookups, which caused infinite recursion when mocking classes like
AbstractList whose hashCode() invokes instrumented methods.
@jselbo
Copy link
Copy Markdown
Collaborator Author

jselbo commented Mar 3, 2026

It seems that IdentityHashMap<MockWeakReference<Object>, ..> is not a functional solution because the mock can be GC'ed but the keys in the map still exist, and it's unclear how we'd do lookups. I think the only way to get both behaviors is a new map impl.

@jselbo jselbo force-pushed the fix-singleton-mock-recursion branch from 432b2f2 to 6707f78 Compare March 3, 2026 21:08
@jselbo jselbo requested a review from raphw March 3, 2026 21:23
Copy link
Copy Markdown
Member

@raphw raphw left a comment

Choose a reason for hiding this comment

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

Last nit pick: add a note to the exception that states that the instance is already mocked that this is for the current thread, to make it obvious.

* lookups. Hash collisions (distinct objects sharing an identity hash code) are resolved by
* linear scan within the bucket using {@code ==}. Lookups do not allocate.
*/
public class WeakIdentityMap<V> extends AbstractMap<Object, V> {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Nitpick: we do not need to implement a full map API for the needs we have, but it does neither hurt, and since it is internal API, we can remove or change this API later anyways. We can keep this for this reason, and it makes the code easier to comprehend, possibly.

@raphw raphw merged commit a231205 into mockito:main Mar 4, 2026
19 checks passed
@jselbo jselbo deleted the fix-singleton-mock-recursion branch March 4, 2026 20:47
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