Fix concurrent group access to prevent NullPointerException
keycloak/keycloak#40940
Golden recall: 100%
Extra findings: 4
Golden Comments (2/2 found)
| # | Comment | Severity | Status | FriendlyReviewer Detail |
|---|---|---|---|---|
| #1 | getSubGroupsCount() returns null — violates the GroupModel contract ("Never returns null") and can cause NPEs on unboxing |
HIGH | ✓ Found | FriendlyReviewer: "Returning null from getSubGroupsCount() violates the GroupModel interface contract... can cause NPEs on unboxing" (GroupAdapter.java L275) |
| #2 | The reader thread is not waited on; deletedAll is toggled to true and assertion is immediate — flaky test |
MEDIUM | ✓ Found | FriendlyReviewer: "Background thread is never joined before assertion, making test flaky. Add thread.join(timeout) before checking exceptions" (GroupTest.java L139) |
Supplementary Findings (5 findings)
| Finding | File | Severity | Legitimate? |
|---|---|---|---|
getDelegateForUpdate() uses modelSupplier.get() which may return a stale cached model if already resolved by a prior read — write-skew on all write operations |
GroupAdapter.java:60 | HIGH | ✓ Real data consistency bug in a PR meant to fix concurrency |
LazyModel not thread-safe — no synchronization on the cached model field, visibility issues under concurrent access |
GroupAdapter.java:54 | MEDIUM | ✓ Genuine thread-safety gap |
isUpdated() has a TOCTOU race — after checking invalidated flag, another thread may set it, returning stale data |
GroupAdapter.java:72 | MEDIUM | ✓ Real race condition in concurrency fix PR |
| Missing cleanup registration for created groups — 100 groups leak if the test fails before deletion | GroupTest.java:118 | MEDIUM | ✓ Test pollution |
Typo in variable name: groupUuuids should be groupUuids |
GroupTest.java:118 | LOW | ✓ Minor |
Honest Analysis
FriendlyReviewer's full review
As posted on the PR — the complete output from FriendlyReviewer.
Hi there 👋
| 🌥️ Tech |
|---|
| 2 high, 4 medium, 1 low |
The code review identified several critical and medium-severity issues. The most significant is a data consistency bug in GroupAdapter.java where getDelegateForUpdate() can use a stale cached delegate model, leading to write-skew. Another critical issue is that getSubGroupsCount() returns null, violating the GroupModel interface contract and risking NPEs. Concurrency concerns include non-thread-safe LazyModel caching and a TOCTOU race in isUpdated(). The new concurrency test in GroupTest.java has multiple problems: missing cleanup registration, a typo, and a lack of thread joining, making it flaky. The GroupUtils.java change is a safe cleanup of unused code.
These might need a close look
- 🔴 model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/GroupAdapter.java (L60)
CRITICAL:getDelegateForUpdate()usesmodelSupplier.get()which may return a stale cached model if already resolved by a prior read. This causes all subsequent write operations to operate on stale data. Replace withgetGroupModel()to always fetch fresh from the database. - 🔴 model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/GroupAdapter.java (L275)
CRITICAL: ReturningnullfromgetSubGroupsCount()violates theGroupModelinterface contract 'Never returns {@code null}'. This can cause NPEs on unboxing. Either throwIllegalStateExceptionor return0Lwhen model is null.
Worth checking
- 🟡 model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/GroupAdapter.java (L54)
MEDIUM:LazyModelis not thread-safe; it lacks synchronization on the cached model field. SinceGroupAdapteris accessed concurrently, this can lead to visibility issues. Consider addingvolatileand DCL or eliminating the caching layer. - 🟡 model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/GroupAdapter.java (L72)
MEDIUM:isUpdated()has a TOCTOU race: after checkinginvalidatedflag, another thread may set it, causing stale data to be returned. Checkinvalidatedagain after fetching from DB or synchronize the read path. - 🟡 tests/base/src/test/java/org/keycloak/tests/admin/group/GroupTest.java (L118)
Missing cleanup registration for created groups. If test fails before deletion, 100 groups leak. Follow the pattern increateGroup()to register cleanup immediately. - 🟡 tests/base/src/test/java/org/keycloak/tests/admin/group/GroupTest.java (L139)
Background thread is never joined before assertion, making test flaky. Addthread.join(timeout)before checking exceptions. Also consider thread cleanup in a finally block.
Small things (take or leave)
- 🔵 tests/base/src/test/java/org/keycloak/tests/admin/group/GroupTest.java (L118)
Typo in variable name:groupUuuidsshould begroupUuids.
FriendlyReviewer detected both golden bugs in this PR. The
getSubGroupsCount()returningnull(#1) was flagged with the correct severity and an accurate description of the contract violation. The flaky test (#2) was also correctly identified with a concrete fix suggestion (thread.join()).The most significant finding FriendlyReviewer contributed beyond the golden set is the write-skew bug in
getDelegateForUpdate(). WhenmodelSupplier.get()returns a stale cached model, all subsequent write operations in that session operate on stale data. This is a real data consistency issue — and notably, it exists in a PR whose stated purpose is precisely to fix concurrent group access.The additional findings on
LazyModelthread-safety and the TOCTOU race inisUpdated()are genuine concurrency concerns that reinforce the same theme: the PR addresses one surface-level bug (the NPE) but leaves deeper structural issues untouched. FriendlyReviewer surfaced all of them.One finding is minor — the
groupUuuidstypo — but it costs nothing to include. The cleanup registration gap for test groups (possible test pollution) is a legitimate medium-severity concern.