Skip to content

Commit 23e94c2

Browse files
authored
Merge pull request #786 from aschackmull/java/double-checked-locking
Java: Fix FP in DoubleCheckedLocking.ql
2 parents fccf30e + 2c0e1f9 commit 23e94c2

File tree

8 files changed

+123
-12
lines changed

8 files changed

+123
-12
lines changed

change-notes/1.20/analysis-java.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
| **Query** | **Expected impact** | **Change** |
1616
|----------------------------|------------------------|------------------------------------------------------------------|
17+
| Double-checked locking is not thread-safe (`java/unsafe-double-checked-locking`) | Fewer false positive results and more true positive results | Results that use safe publication through a `final` field are no longer reported. Results that initialize immutable types like `String` incorrectly are now reported. |
1718
| Result of multiplication cast to wider type (`java/integer-multiplication-cast-to-long`) | Fewer results | Results involving conversions to `float` or `double` are no longer reported, as they were almost exclusively false positives. |
1819

1920
## Changes to QL libraries

java/ql/src/Likely Bugs/Concurrency/DoubleCheckedLocking.ql

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,26 @@
1515
import java
1616
import DoubleCheckedLocking
1717

18+
predicate allFieldsFinal(Class c) { forex(Field f | c.inherits(f) | f.isFinal()) }
19+
20+
predicate immutableFieldType(Type t) {
21+
allFieldsFinal(t) or
22+
t instanceof ImmutableType
23+
}
24+
1825
from IfStmt if1, IfStmt if2, SynchronizedStmt sync, Field f
1926
where
2027
doubleCheckedLocking(if1, if2, sync, f) and
21-
not f.isVolatile()
28+
not f.isVolatile() and
29+
not (
30+
// Non-volatile double-checked locking is ok when the object is immutable and
31+
// there is only a single non-synchronized field read.
32+
immutableFieldType(f.getType()) and
33+
1 = strictcount(FieldAccess fa |
34+
fa.getField() = f and
35+
fa.getEnclosingCallable() = sync.getEnclosingCallable() and
36+
not fa.getEnclosingStmt().getParent*() = sync.getBlock()
37+
)
38+
)
2239
select sync, "Double-checked locking on the non-volatile field $@ is not thread-safe.", f,
2340
f.toString()

java/ql/src/Likely Bugs/Concurrency/DoubleCheckedLocking.qll

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,5 @@ predicate doubleCheckedLocking(IfStmt if1, IfStmt if2, SynchronizedStmt sync, Fi
3838
if1.getThen() = sync.getParent*() and
3939
sync.getBlock() = if2.getParent*() and
4040
if1.getCondition() = getANullCheck(f) and
41-
if2.getCondition() = getANullCheck(f) and
42-
not f.getType() instanceof ImmutableType
41+
if2.getCondition() = getANullCheck(f)
4342
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
private Object lock = new Object();
2+
private MyImmutableObject f = null;
3+
4+
public MyImmutableObject getMyImmutableObject() {
5+
if (f == null) {
6+
synchronized(lock) {
7+
if (f == null) {
8+
f = new MyImmutableObject();
9+
}
10+
}
11+
}
12+
return f; // BAD
13+
}

java/ql/src/Likely Bugs/Concurrency/DoubleCheckedLockingShared.qhelp

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,26 @@ variable can be used to avoid reading the field more times than neccessary.
6666
</p>
6767
<sample src="DoubleCheckedLockingGood.java"/>
6868

69+
<p>
70+
As a final note, it is possible to use double-checked locking correctly without
71+
<code>volatile</code> if the object you construct is immutable (that is, the
72+
object declares all fields as <code>final</code>), and the double-checked field
73+
is read exactly once outside the synchronized block.
74+
</p>
75+
<p>
76+
Given that all fields in <code>MyImmutableObject</code> are declared
77+
<code>final</code> then the following example is protected against exposing
78+
uninitialized fields to another thread. However, since there are two reads of
79+
<code>f</code> without synchronization, it is possible that these are
80+
reordered, which means that this method can return <code>null</code>.
81+
</p>
82+
<sample src="DoubleCheckedLockingBad3.java"/>
83+
<p>
84+
In this case, using a local variable to minimize the number of field reads is
85+
no longer a performance improvement, but rather a crucial detail that is
86+
necessary for correctness.
87+
</p>
88+
6989
</example>
7090

7191
<references>
@@ -80,6 +100,14 @@ Java Language Specification:
80100
<li>
81101
Wikipedia: <a href="https://en.wikipedia.org/wiki/Double-checked_locking">Double-checked locking</a>.
82102
</li>
103+
<li>
104+
Aleksey Shipilëv:
105+
<a href="https://shipilev.net/blog/2014/safe-public-construction/">Safe Publication and Safe Initialization in Java</a>.
106+
</li>
107+
<li>
108+
Aleksey Shipilëv:
109+
<a href="https://shipilev.net/blog/2016/close-encounters-of-jmm-kind/">Close Encounters of The Java Memory Model Kind</a>.
110+
</li>
83111

84112
</references>
85113

java/ql/test/query-tests/DoubleCheckedLocking/A.java

Lines changed: 57 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,31 @@ public void setX(int x) {
66
}
77
}
88

9-
private String s;
10-
public String getString() {
11-
if (s == null) {
9+
private String s1;
10+
public String getString1() {
11+
if (s1 == null) {
1212
synchronized(this) {
13-
if (s == null) {
14-
s = "string"; // OK, immutable
13+
if (s1 == null) {
14+
s1 = "string"; // BAD, immutable but read twice outside sync
1515
}
1616
}
1717
}
18-
return s;
18+
return s1;
19+
}
20+
21+
private String s2;
22+
public String getString2() {
23+
String x = s2;
24+
if (x == null) {
25+
synchronized(this) {
26+
x = s2;
27+
if (x == null) {
28+
x = "string"; // OK, immutable and read once outside sync
29+
s2 = x;
30+
}
31+
}
32+
}
33+
return x;
1934
}
2035

2136
private B b1;
@@ -72,4 +87,40 @@ public B getter4() {
7287
}
7388
return b4;
7489
}
90+
91+
static class FinalHelper<T> {
92+
public final T x;
93+
public FinalHelper(T x) {
94+
this.x = x;
95+
}
96+
}
97+
98+
private FinalHelper<B> b5;
99+
public B getter5() {
100+
if (b5 == null) {
101+
synchronized(this) {
102+
if (b5 == null) {
103+
B b = new B();
104+
b5 = new FinalHelper<B>(b); // BAD, racy read on b5 outside synchronized-block
105+
}
106+
}
107+
}
108+
return b5.x; // Potential NPE here, as the two b5 reads may be reordered
109+
}
110+
111+
private FinalHelper<B> b6;
112+
public B getter6() {
113+
FinalHelper<B> a = b6;
114+
if (a == null) {
115+
synchronized(this) {
116+
a = b6;
117+
if (a == null) {
118+
B b = new B();
119+
a = new FinalHelper<B>(b);
120+
b6 = a; // OK, published through final field with a single non-synced read
121+
}
122+
}
123+
}
124+
return a.x;
125+
}
75126
}
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
1-
| A.java:25:7:30:7 | stmt | Double-checked locking on the non-volatile field $@ is not thread-safe. | A.java:21:13:21:14 | b1 | b1 |
1+
| A.java:12:7:16:7 | stmt | Double-checked locking on the non-volatile field $@ is not thread-safe. | A.java:9:18:9:19 | s1 | s1 |
2+
| A.java:40:7:45:7 | stmt | Double-checked locking on the non-volatile field $@ is not thread-safe. | A.java:36:13:36:14 | b1 | b1 |
3+
| A.java:101:7:106:7 | stmt | Double-checked locking on the non-volatile field $@ is not thread-safe. | A.java:98:26:98:27 | b5 | b5 |
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
| A.java:55:11:55:22 | ...=... | Potential race condition. This assignment to $@ is visible to other threads before the subsequent statements are executed. | A.java:50:22:50:23 | b3 | b3 |
2-
| A.java:68:11:68:22 | ...=... | Potential race condition. This assignment to $@ is visible to other threads before the subsequent statements are executed. | A.java:63:22:63:23 | b4 | b4 |
1+
| A.java:70:11:70:22 | ...=... | Potential race condition. This assignment to $@ is visible to other threads before the subsequent statements are executed. | A.java:65:22:65:23 | b3 | b3 |
2+
| A.java:83:11:83:22 | ...=... | Potential race condition. This assignment to $@ is visible to other threads before the subsequent statements are executed. | A.java:78:22:78:23 | b4 | b4 |

0 commit comments

Comments
 (0)