Repository navigation
Expand file tree
/
Copy pathConflictingAccess.qll
More file actions
297 lines (275 loc) · 11.7 KB
/
Copy pathConflictingAccess.qll
File metadata and controls
297 lines (275 loc) · 11.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
/**
* Provides classes and predicates for detecting conflicting accesses in the sense of the Java Memory Model.
*/
overlay[local?]
module;
import java
import Concurrency
/** Provides predicates, chiefly `isModifying`, to check if a given expression modifies a shared resource. */
module Modification {
import semmle.code.java.dataflow.FlowSummary
/** Holds if the field access `a` modifies a shared resource. */
predicate isModifying(FieldAccess a) {
a.isVarWrite()
or
exists(Call c | c.(MethodCall).getQualifier() = a | isModifyingCall(c))
or
exists(ArrayAccess aa, Assignment asa | aa.getArray() = a | asa.getDest() = aa)
}
/** Holds if the call `c` modifies a shared resource. */
predicate isModifyingCall(Call c) {
exists(SummarizedCallable sc, string output | sc.getACall() = c |
sc.propagatesFlow(_, output, _, _, _, _) and
output.matches("Argument[this]%")
)
}
}
/** Holds if the type `t` is thread-safe. */
predicate isThreadSafeType(Type t) {
t.(RefType).getSourceDeclaration().getName().matches(["Atomic%", "Concurrent%"])
or
t.(RefType).getSourceDeclaration().getName() = "ThreadLocal"
or
// Anything in `java.util.concurrent` is thread safe.
// See https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/package-summary.html#MemoryVisibility
t.(RefType).getPackage().getName() = "java.util.concurrent"
or
t instanceof ClassAnnotatedAsThreadSafe
}
/** Holds if the expression `e` is a thread-safe initializer. */
private predicate isThreadSafeInitializer(Expr e) {
exists(string name |
e.(Call).getCallee().getSourceDeclaration().hasQualifiedName("java.util", "Collections", name)
|
name.matches("synchronized%")
)
}
/**
* A field that is exposed to potential data races.
* We require the field to be in a class that is annotated as `@ThreadSafe`.
*/
class ExposedField extends Field {
ExposedField() {
this.getDeclaringType() instanceof ClassAnnotatedAsThreadSafe and
not this.isVolatile() and
// field is not a lock
not this.getType() instanceof LockType and
// field is not thread-safe
not isThreadSafeType(this.getType()) and
not isThreadSafeType(initialValue(this).getType()) and
// the initializer guarantees thread safety
not isThreadSafeInitializer(initialValue(this))
}
}
/**
* Gets the initial value for the field `f`.
* This is either a field initializer or an assignment in a constructor.
*/
Expr initialValue(Field f) {
result = f.getInitializer()
or
result = f.getAnAssignedValue() and
result.getEnclosingCallable() = f.getDeclaringType().getAConstructor()
}
/**
* A field access that is exposed to potential data races.
* We require the field to be in a class that is annotated as `@ThreadSafe`.
*/
class ExposedFieldAccess extends FieldAccess {
ExposedFieldAccess() {
// access is to an exposed field
this.getField() instanceof ExposedField and
// access is not the initializer of the field
not this.(VarWrite).getASource() = this.getField().getInitializer() and
// access not in a constructor
not this.getEnclosingCallable() = this.getField().getDeclaringType().getAConstructor() and
// not a field on a local variable
not this.getQualifier+().(VarAccess).getVariable() instanceof LocalVariableDecl and
// not the variable mentioned in a synchronized statement
not this = any(SynchronizedStmt sync).getExpr()
}
}
/**
* A class annotated as `@ThreadSafe`.
* Provides predicates to check for concurrency issues.
*/
class ClassAnnotatedAsThreadSafe extends Class {
ClassAnnotatedAsThreadSafe() { this.getAnAnnotation().getType().getName() = "ThreadSafe" }
// We wish to find conflicting accesses that are reachable from public methods
// and to know which monitors protect them.
//
// It is very easy and natural to write a predicate for conflicting accesses,
// but that would be binary, and hence not suited for reachability analysis.
//
// It is also easy to state that all accesses to a field are protected by a single monitor,
// but that would require a forall, which is not suited for recursion.
// (The recursion occurs for example as you traverse the access path and keep requiring that all tails are protected.)
//
// We therefore use a dual solution:
// - We write a unary recursive predicate for accesses that are not protected by any monitor.
// Any such write access, reachable from a public method, is conflicting with itself.
// And any such read will be conflicting with any publicly reachable write access (locked or not).
//
// - We project the above predicate down to fields, so we can find fields with unprotected accesses.
// - From this we can derive a unary recursive predicate for fields whose accesses are protected by exactly one monitor.
// The predicate tracks the monitor.
// If such a field has two accesses protected by different monitors, we have a concurrency issue.
// This can be determined by simple counting at the end of the recursion.
// Technically, we only have a concurrency issue if there is a write access,
// but if you are locking your reads with different locks, you likely made a typo.
//
// - From the above, we can derive a unary recursive predicate for fields whose accesses are protected by at least one monitor.
// This predicate tracks all the monitors that protect accesses to the field.
// This is combined with a predicate that checks if any access escapes a given monitor.
// If all the monitors that protect accesses to a field are escaped by at least one access,
// we have a concurrency issue.
// This can be determined by a single forall at the end of the recursion.
//
// With this formulation we avoid binary predicates and foralls in recursion.
//
// Cases where a field access is not protected by any monitor
/**
* Holds if the field access `a` to the field `f` is not protected by any monitor, and it can be reached via the expression `e` in the method `m`.
* We maintain the invariant that `m = e.getEnclosingCallable()`.
*/
private predicate unlockedAccess(
ExposedField f, Expr e, Method m, ExposedFieldAccess a, boolean write
) {
m.getDeclaringType() = this and
(
// base case
f.getDeclaringType() = this and
m = e.getEnclosingCallable() and
a.getField() = f and
a = e and
(if Modification::isModifying(a) then write = true else write = false)
or
// recursive case
exists(MethodCall c, Expr e0, Method m0 | this.unlockedAccess(f, e0, m0, a, write) |
m = c.getEnclosingCallable() and
not m0.isPublic() and
c.getCallee().getSourceDeclaration() = m0 and
c = e and
not Monitors::locallyMonitors(e0, _)
)
)
}
/** Holds if the class has an unlocked access to the field `f` via the expression `e` in the method `m`. */
private predicate hasUnlockedAccess(ExposedField f, Expr e, Method m, boolean write) {
this.unlockedAccess(f, e, m, _, write)
}
/** Holds if the field access `a` to the field `f` is not protected by any monitor, and it can be reached via the expression `e` in the public method `m`. */
predicate unlockedPublicAccess(
ExposedField f, Expr e, Method m, ExposedFieldAccess a, boolean write
) {
this.unlockedAccess(f, e, m, a, write) and
m.isPublic() and
not Monitors::locallyMonitors(e, _)
}
/** Holds if the class has an unlocked access to the field `f` via the expression `e` in the public method `m`. */
private predicate hasUnlockedPublicAccess(ExposedField f, Expr e, Method m, boolean write) {
this.unlockedPublicAccess(f, e, m, _, write)
}
// Cases where all accesses to a field are protected by exactly one monitor
//
/**
* Holds if the class has an access, locked by exactly one monitor, to the field `f` via the expression `e` in the method `m`.
*/
private predicate hasOnelockedAccess(
ExposedField f, Expr e, Method m, boolean write, Monitors::Monitor monitor
) {
//base
this.hasUnlockedAccess(f, e, m, write) and
Monitors::locallyMonitors(e, monitor)
or
// recursive case
exists(MethodCall c, Method m0 | this.hasOnelockedAccess(f, _, m0, write, monitor) |
m = c.getEnclosingCallable() and
not m0.isPublic() and
c.getCallee().getSourceDeclaration() = m0 and
c = e and
// consider allowing idempotent monitors
not Monitors::locallyMonitors(e, _) and
m.getDeclaringType() = this
)
}
/** Holds if the class has an access, locked by exactly one monitor, to the field `f` via the expression `e` in the public method `m`. */
private predicate hasOnelockedPublicAccess(
ExposedField f, Expr e, Method m, boolean write, Monitors::Monitor monitor
) {
this.hasOnelockedAccess(f, e, m, write, monitor) and
m.isPublic() and
not this.hasUnlockedPublicAccess(f, e, m, write)
}
/** Holds if the field `f` has more than one access, all locked by a single monitor, but different monitors are used. */
predicate singleMonitorMismatch(ExposedField f) {
2 <= strictcount(Monitors::Monitor monitor | this.hasOnelockedPublicAccess(f, _, _, _, monitor))
}
// Cases where all accesses to a field are protected by at least one monitor
//
/** Holds if the class has an access, locked by at least one monitor, to the field `f` via the expression `e` in the method `m`. */
private predicate hasOnepluslockedAccess(
ExposedField f, Expr e, Method m, boolean write, Monitors::Monitor monitor
) {
//base
this.hasOnelockedAccess(f, e, m, write, monitor) and
not this.singleMonitorMismatch(f) and
not this.hasUnlockedPublicAccess(f, _, _, _)
or
// recursive case
exists(MethodCall c, Method m0, Monitors::Monitor monitor0 |
this.hasOnepluslockedAccess(f, _, m0, write, monitor0) and
m = c.getEnclosingCallable() and
not m0.isPublic() and
c.getCallee().getSourceDeclaration() = m0 and
c = e and
m.getDeclaringType() = this
|
monitor = monitor0
or
Monitors::locallyMonitors(e, monitor)
)
}
/** Holds if the class has a write access to the field `f` that can be reached via a public method. */
predicate hasPublicWriteAccess(ExposedField f) {
this.hasUnlockedPublicAccess(f, _, _, true)
or
this.hasOnelockedPublicAccess(f, _, _, true, _)
or
exists(Method m | m.getDeclaringType() = this and m.isPublic() |
this.hasOnepluslockedAccess(f, _, m, true, _)
)
}
/** Holds if the class has an access, not protected by the monitor `m`, to the field `f` via the expression `e` in the method `m`. */
private predicate escapesMonitor(
ExposedField f, Expr e, Method m, boolean write, Monitors::Monitor monitor
) {
//base
this.hasOnepluslockedAccess(f, _, _, _, monitor) and
this.hasUnlockedAccess(f, e, m, write) and
not Monitors::locallyMonitors(e, monitor)
or
// recursive case
exists(MethodCall c, Method m0 | this.escapesMonitor(f, _, m0, write, monitor) |
m = c.getEnclosingCallable() and
not m0.isPublic() and
c.getCallee().getSourceDeclaration() = m0 and
c = e and
not Monitors::locallyMonitors(e, monitor) and
m.getDeclaringType() = this
)
}
/** Holds if the class has an access, not protected by the monitor `m`, to the field `f` via the expression `e` in the public method `m`. */
private predicate escapesMonitorPublic(
ExposedField f, Expr e, Method m, boolean write, Monitors::Monitor monitor
) {
this.escapesMonitor(f, e, m, write, monitor) and
m.isPublic()
}
/** Holds if no monitor protects all accesses to the field `f`. */
predicate notFullyMonitored(ExposedField f) {
forex(Monitors::Monitor monitor | this.hasOnepluslockedAccess(f, _, _, _, monitor) |
this.escapesMonitorPublic(f, _, _, _, monitor)
)
}
}