JDK 8 vs 17 vs 21: How OpenJDK Fixed the C2 Compiler Infinite Loop on AArch64
tl;dr: We compared MemNode::can_see_stored_value across OpenJDK versions using the actual source code. The fix — removing Op_MemBarCPUOrder from the skip list — was introduced in JDK 18 (JDK-8223923) and backported to JDK 17.0.2 and JDK 11.0.14, but never to JDK 8u. The blog post previously claimed JDK 17 was still vulnerable because it only checked 17+35 (the GA tag); the actual fix landed in 17.0.2 via backport JDK-8274488. None of the versions include an explicit depth guard.
Background
In a previous investigation, we identified a C2 JIT compiler infinite loop that occurs on AArch64 when running OpenJDK 8u442. The infinite loop is inside MemNode::can_see_stored_value in memnode.cpp, where a while loop walks through memory barrier (MemBar) and memory merge (MergeMem) nodes. Under certain conditions — specifically when dead code elimination creates a cycle involving MemBarCPUOrder nodes — the loop spins forever.
We resolved the issue in production by switching to Alibaba Dragonwell 8, which includes a depth guard patch. But a natural follow-up question remained:
Was this bug fixed in newer JDK versions? If so, how?
To answer this, we performed a line-by-line comparison of can_see_stored_value using the actual source code across multiple versions:
- OpenJDK 8u502-b05 — vulnerable, fix never backported
- OpenJDK 17+35 (GA) — vulnerable (pre-fix GA)
- OpenJDK 17.0.2+1 — fixed (backport JDK-8274488)
- OpenJDK 21+35 — fixed (original fix in JDK 18)
1. The Vulnerable Loop: What It Does
In all versions, can_see_stored_value contains a while loop that walks backward through memory barrier chains to find the actual memory state for a given alias index. The structure is:
while (current->is_Proj()) {
int opc = current->in(0)->Opcode();
if (/* opc matches certain MemBar types */) {
Node* mem = current->in(0)->in(TypeFunc::Memory);
if (mem->is_MergeMem()) {
MergeMemNode* merge = mem->as_MergeMem();
Node* new_st = merge->memory_at(alias_idx);
if (new_st == merge->base_memory()) {
// Keep searching
current = new_st;
continue; // ← potential infinite loop
}
result = new_st;
}
}
break;
}
The danger is in the continue path: if merge->memory_at(alias_idx) returns merge->base_memory(), and that node happens to be (or eventually leads back to) the same Proj node we started from, the loop never terminates.
The question is: which MemBar opcodes are included in the condition?
2. JDK 8u502: The Vulnerable Version (Never Fixed)
// OpenJDK 8u502-b05
while (current->is_Proj()) {
int opc = current->in(0)->Opcode();
if ((final && (opc == Op_MemBarAcquire ||
opc == Op_MemBarAcquireLock ||
opc == Op_LoadFence)) ||
opc == Op_MemBarRelease ||
opc == Op_StoreFence ||
opc == Op_MemBarReleaseLock ||
opc == Op_MemBarCPUOrder) { // ← present, VULNERABLE
// ... continue path possible → infinite loop
}
break;
}
Op_MemBarCPUOrderis in the condition — the loop enters the body when it encounters aMemBarCPUOrdernode- No depth guard
- Fix status: JDK-8223923 was NEVER backported to 8u
3. JDK 17+35 (GA): Also Vulnerable
// OpenJDK 17+35 (GA, pre-fix)
while (current->is_Proj()) {
int opc = current->in(0)->Opcode();
if ((final && (opc == Op_MemBarAcquire ||
opc == Op_MemBarAcquireLock ||
opc == Op_LoadFence)) ||
opc == Op_MemBarRelease ||
opc == Op_StoreFence ||
opc == Op_MemBarReleaseLock ||
opc == Op_MemBarStoreStore || // ← newly added in JDK 17
opc == Op_MemBarCPUOrder) { // ← still present, VULNERABLE
// ... continue path possible → infinite loop
}
break;
}
The 17+35 GA tag added Op_MemBarStoreStore but kept Op_MemBarCPUOrder. This is the version many people think of as "JDK 17" — and it is vulnerable.
4. JDK 17.0.2: Fixed via Backport
// OpenJDK 17.0.2+1 — backport of JDK-8223923 (JDK-8274488)
// Comment added:
// "It is not safe to step over MemBarCPUOrder, because alias info
// above them may be inaccurate (e.g., due to mixed/mismatched
// unsafe accesses)."
bool is_final_mem = !atp->is_rewritable();
while (current->is_Proj()) {
int opc = current->in(0)->Opcode();
if ((is_final_mem && (opc == Op_MemBarAcquire ||
opc == Op_MemBarAcquireLock ||
opc == Op_LoadFence)) ||
opc == Op_MemBarRelease ||
opc == Op_StoreFence ||
opc == Op_MemBarReleaseLock ||
opc == Op_MemBarStoreStore) { // ← Op_MemBarCPUOrder REMOVED
// ... loop body
}
break;
}
What changed
Op_MemBarCPUOrderhas been REMOVED — this is the core fix- Variable renamed from
finaltois_final_mem - Comment added explaining why
MemBarCPUOrderwas removed - Backport reference: JDK-8274488, resolved in 17.0.2+1
Correction notice: A previous version of this article claimed "JDK 17 is still vulnerable" based on the 17+35 GA tag. That was incorrect for 17.0.2+. The fix was backported to JDK 17.0.2 via JDK-8274488. If you are running JDK 17.0.2 or later, you are protected.
5. JDK 21+35: Same Fix, Plus StoreStoreFence
// OpenJDK 21+35
bool is_final_mem = !atp->is_rewritable();
while (current->is_Proj()) {
int opc = current->in(0)->Opcode();
if ((is_final_mem && (opc == Op_MemBarAcquire ||
opc == Op_MemBarAcquireLock ||
opc == Op_LoadFence)) ||
opc == Op_MemBarRelease ||
opc == Op_StoreFence ||
opc == Op_MemBarReleaseLock ||
opc == Op_MemBarStoreStore ||
opc == Op_StoreStoreFence) { // ← new fence type added
// ← Op_MemBarCPUOrder still absent
// ... loop body
}
break;
}
JDK 21 adds Op_StoreStoreFence (a new weaker fence type) but keeps Op_MemBarCPUOrder excluded. The fix from JDK 18 is preserved.
6. The Execution Path Difference
JDK 8u / JDK 17.0.1 and earlier (Vulnerable)
current = Proj node
↓
current->in(0)->Opcode() == Op_MemBarCPUOrder
↓
if condition: TRUE (MemBarCPUOrder is in the list)
↓
mem = MergeMem
↓
new_st = merge->memory_at(alias_idx)
↓
new_st == merge->base_memory()? YES
↓
current = new_st (back to same Proj node)
↓
continue → back to while
↓
INFINITE LOOP
JDK 17.0.2+ / JDK 21 (Fixed)
current = Proj node
↓
current->in(0)->Opcode() == Op_MemBarCPUOrder
↓
if condition: FALSE (MemBarCPUOrder is NOT in the list)
↓
break → EXIT LOOP
↓
SAFE
7. Why Was MemBarCPUOrder Removed?
The JDK 17.0.2 / JDK 18+ source includes this comment directly above the while loop:
It is not safe to step over MemBarCPUOrder, because alias info above them may be inaccurate (e.g., due to mixed/mismatched unsafe accesses).
Correctness concern: Skipping over MemBarCPUOrder can lead to incorrect alias analysis. MemBarCPUOrder is used in contexts such as Unsafe memory accesses and object allocation barriers, where the memory state above the barrier may not accurately reflect the true aliasing relationships.
Safety concern: The cycle we discovered — where MergeMem.base_memory() points back to the current Proj node — is a direct consequence of walking through MemBarCPUOrder under these conditions. By refusing to step over MemBarCPUOrder, the loop avoids entering the dangerous continue path entirely.
8. Complete Comparison
Opcode Conditions
| Opcode | JDK 8u (all) | JDK 17+35 (GA) | JDK 17.0.2+ | JDK 11.0.14+ | JDK 21+ |
|---|---|---|---|---|---|
Op_MemBarAcquire (final only) |
✅ | ✅ | ✅ | ✅ | ✅ |
Op_MemBarAcquireLock (final only) |
✅ | ✅ | ✅ | ✅ | ✅ |
Op_LoadFence (final only) |
✅ | ✅ | ✅ | ✅ | ✅ |
Op_MemBarRelease |
✅ | ✅ | ✅ | ✅ | ✅ |
Op_StoreFence |
✅ | ✅ | ✅ | ✅ | ✅ |
Op_MemBarReleaseLock |
✅ | ✅ | ✅ | ✅ | ✅ |
Op_MemBarStoreStore |
❌ | ✅ added | ✅ | depends on 11u version | ✅ |
Op_MemBarCPUOrder |
✅ vulnerable | ✅ vulnerable | ❌ removed (FIXED) | ❌ removed (FIXED) | ❌ removed |
Op_StoreStoreFence |
❌ | ❌ | ❌ | ❌ | ✅ added |
| Variable name | final |
final |
is_final_mem |
final |
is_final_mem |
| Depth guard | ❌ | ❌ | ❌ | ❌ | ❌ |
Fix Timeline
JDK 8u (all versions):
= Op_MemBarCPUOrder present ← VULNERABLE (fix never backported)
JDK 11.0.13 and earlier:
= Op_MemBarCPUOrder present ← VULNERABLE
JDK 11.0.14 (Jan 2022):
- Removed Op_MemBarCPUOrder ← FIXED (JDK-8274713)
JDK 17+35 / 17.0.1 (GA, 2021):
= Op_MemBarCPUOrder present ← VULNERABLE
JDK 17.0.2 (Jan 2022):
- Removed Op_MemBarCPUOrder ← FIXED (JDK-8274488)
JDK 18 (2022):
- Removed Op_MemBarCPUOrder ← FIXED (original fix JDK-8223923)
JDK 21+:
- Op_MemBarCPUOrder still absent ← FIXED
+ Added Op_StoreStoreFence
Safety Properties
| Property | JDK 8u | JDK 17+35 GA | JDK 17.0.2+ | JDK 21+ |
|---|---|---|---|---|
| Enters body on MemBarCPUOrder | 🔴 Yes | 🔴 Yes | 🟢 No | 🟢 No |
| Depth guard | ❌ No | ❌ No | ❌ No | ❌ No |
| Vulnerable to MemBarCPUOrder cycle | 🔴 Yes | 🔴 Yes | 🟢 No | 🟢 No |
| AArch64 production risk | 🔴 High | 🔴 High | 🟢 Very Low | 🟢 Very Low |
9. What About the Depth Guard?
None of the versions include a depth guard in this while loop.
| Version | Depth Guard | Cycle Detection |
|---|---|---|
| JDK 8u (all) | ❌ No | ❌ No |
| JDK 17 (all) | ❌ No | ❌ No |
| JDK 21+ | ❌ No | ❌ No |
| Dragonwell 8 | ✅ Yes | ✅ Yes |
The upstream strategy is prevention (remove the dangerous opcode so the loop never enters the risky path) rather than detection (notice you are in a loop and break out).
Dragonwell 8's depth guard provides defense in depth: even if a graph cycle forms for an unexpected reason, the compiler will bail out instead of hanging forever.
10. Implications
For JDK 8u users on AArch64
You are running the most vulnerable version. The fix from JDK 18 (removing Op_MemBarCPUOrder) has never been backported to JDK 8u.
Your options are:
- Switch to Alibaba Dragonwell 8 — includes a depth guard patch that prevents the infinite loop
- Apply the backport patch yourself — available here, a one-line removal of
Op_MemBarCPUOrder - Exclude known triggering methods:
-XX:CompileCommand=exclude,org/springframework/beans/BeanPropertyHandler::toTypeDescriptor - Upgrade to JDK 17.0.2+ or JDK 21+ — the root cause is addressed
For JDK 17 users on AArch64
- 17.0.1 and earlier (including 17+35 GA): Vulnerable. Upgrade to 17.0.2+ immediately.
- 17.0.2 and later: Protected. The fix was backported in JDK-8274488.
For JDK 11 users on AArch64
- 11.0.13 and earlier: Vulnerable. Upgrade to 11.0.14+.
- 11.0.14 and later: Protected. The fix was backported in JDK-8274713.
For JDK 21+ users on AArch64
You are protected against the specific MemBarCPUOrder cycle. However, the loop still lacks a general depth guard.
11. Recommendations
Short-term: Backport the fix to JDK 8u
The minimal fix is to remove Op_MemBarCPUOrder from the while loop condition in can_see_stored_value. This is exactly what JDK 17.0.2 and JDK 11.0.14 did. A ready-to-apply patch is available:
Long-term: Add a depth guard to all versions
Even with MemBarCPUOrder excluded, the while loop has no protection against cycles formed through other barrier types. A depth guard similar to what Dragonwell 8 implements would provide defense in depth.
Conclusion
The C2 compiler infinite loop we encountered on AArch64 was caused by MemNode::can_see_stored_value skipping over MemBarCPUOrder nodes, creating a graph cycle that led to an infinite loop.
The fix — removing Op_MemBarCPUOrder from the skip list — was:
- Introduced in JDK 18 (JDK-8223923)
- Backported to JDK 17.0.2 (JDK-8274488)
- Backported to JDK 11.0.14 (JDK-8274713)
- Never backported to JDK 8u
If you are running JDK 8u on AArch64, you are still vulnerable. Use Dragonwell 8, apply the one-line backport patch, or exclude the triggering methods.
Source code references: - OpenJDK 8u502-b05 memnode.cpp - OpenJDK 17+35 memnode.cpp (GA, vulnerable) - OpenJDK 17.0.2+1 memnode.cpp (fixed) - OpenJDK 21+35 memnode.cpp - Upstream bug: JDK-8223923 - 17u backport: JDK-8274488 - 11u backport: JDK-8274713 - Backport patch for JDK 8u
Tags: JVM Internals, C2 Compiler, AArch64, OpenJDK, Version Comparison, JDK-8223923