CWE-609 describes double-checked locking used to access a resource without the cost of explicit synchronization, where the locking turns out to be insufficient. MITRE lists Java as the affected language.
How it happens
The pattern checks whether a resource is initialized, takes a lock, checks again, and then initializes it if needed. The aim is to avoid locking on every access.
// Vulnerable pattern (Java before 1.5, or without volatile)
class Holder {
private static Helper helper;
static Helper get() {
if (helper == null) { // first check, no lock
synchronized (Holder.class) {
if (helper == null) { // second check
helper = new Helper(); // may be seen half-built
}
}
}
return helper;
}
}
The record explains why this is not safe. It is not guaranteed to work in all languages and on all architectures. Other threads may not be operating inside the synchronized block, and are not guaranteed to see the operations execute in the same order as they appear inside it. A thread can see a non-null reference to an object whose fields are not yet set.
What an attacker gains
MITRE lists integrity impact: application data modified and execution logic altered. These bugs are intermittent and hard to reproduce. Under load, an attacker who can drive many concurrent requests may raise the odds of hitting the window, with effects that range from wrong data to broken security objects, such as a half-configured security component.
How to find it
Static analysis (SAST) can find some instances by recognising the double check shape. Code review should flag any lazy initialization that reads a shared field outside a lock.
How to fix it
MITRE's guidance for Java:
- Double-checked locking is inherently flawed in Java before 1.5. Before that, only the
synchronizedkeyword is known to work. - Beginning in Java 1.5, the
volatilekeyword allows double-checked locking to work, although there is debate about whether it gains enough performance.
private static volatile Helper helper; // volatile makes the pattern correct
Often the simplest option is better: synchronize the accessor, or let the class loader do the work with a holder class, or initialize eagerly.
class Holder {
private static class Lazy { static final Helper INSTANCE = new Helper(); }
static Helper get() { return Lazy.INSTANCE; }
}
Quick check
What does volatile change from Java 1.5 onward?
It lets double-checked locking work correctly, because writes become visible in order to other threads.