-
Notifications
You must be signed in to change notification settings - Fork 2k
Expand file tree
/
Copy pathPointsToContext.qll
More file actions
263 lines (223 loc) · 8.37 KB
/
PointsToContext.qll
File metadata and controls
263 lines (223 loc) · 8.37 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
import python
private import semmle.python.pointsto.PointsTo
private import semmle.python.objects.ObjectInternal
private import semmle.python.types.ImportTime
private import semmle.python.types.Version
/*
* A note on 'cost'. Cost doesn't represent the cost to compute,
* but (a vague estimate of) the cost to compute per value gained.
* This is constantly evolving, so see the various cost functions below for more details.
*/
private int given_cost() {
exists(string depth |
py_flags_versioned("context.cost", depth, _) and
result = depth.toInt()
)
}
pragma[noinline]
private int max_context_cost() {
not py_flags_versioned("context.cost", _, _) and result = 7
or
result = max(int cost | cost = given_cost() | cost)
}
private int syntactic_call_count(Scope s) {
exists(Function f, string name | f = s and name = f.getName() and name != "__init__" |
result = count(function_call(name)) + count(method_call(name))
)
or
s.getName() = "__init__" and result = 1
or
not s instanceof Function and result = 0
}
pragma[nomagic]
private CallNode function_call(string name) { result.getFunction().(NameNode).getId() = name }
pragma[nomagic]
private CallNode method_call(string name) { result.getFunction().(AttrNode).getName() = name }
private int incoming_call_cost(Scope s) {
/*
* Syntactic call count will often be a considerable overestimate
* of the actual number of calls, so we use the square root.
* Cost = log(sqrt(call-count))
*/
result = ((syntactic_call_count(s) + 1).log(2) * 0.5).floor()
}
private int context_cost(TPointsToContext ctx) {
ctx = TMainContext() and result = 0
or
ctx = TRuntimeContext() and result = 0
or
ctx = TImportContext() and result = 0
or
ctx = TCallContext(_, _, result)
}
private int call_cost(CallNode call) {
if call.getScope().inSource() then result = 2 else result = 3
}
private int outgoing_calls(Scope s) { result = strictcount(CallNode call | call.getScope() = s) }
predicate super_method_call(CallNode call) {
call.getFunction().(AttrNode).getObject().(CallNode).getFunction().(NameNode).getId() = "super"
}
private int outgoing_call_cost(CallNode c) {
/* Cost = log(outgoing-call-count) */
result = outgoing_calls(c.getScope()).log(2).floor()
}
/**
* Cost of contexts for a call, the more callers the
* callee of call has the more expensive it is to add contexts for it.
* This seems to be an effective heuristics for preventing an explosion
* in the number of contexts while retaining good results.
*/
private int splay_cost(CallNode c) {
if super_method_call(c)
then result = 0
else result = outgoing_call_cost(c) + incoming_call_cost(c.getScope())
}
private predicate call_to_init_or_del(CallNode call) {
exists(string mname | mname = "__init__" or mname = "__del__" |
mname = call.getFunction().(AttrNode).getName()
)
}
/** Total cost estimate */
private int total_call_cost(CallNode call) {
/*
* We want to always follow __init__ and __del__ calls as they tell us about object construction,
* but we need to be aware of cycles, so they must have a non-zero cost.
*/
if call_to_init_or_del(call) then result = 1 else result = call_cost(call) + splay_cost(call)
}
pragma[nomagic]
private int relevant_call_cost(PointsToContext ctx, CallNode call) {
ctx.appliesTo(call) and result = total_call_cost(call)
}
pragma[noinline]
private int total_cost(CallNode call, PointsToContext ctx) {
result = relevant_call_cost(ctx, call) + context_cost(ctx)
}
cached
private newtype TPointsToContext =
TMainContext() or
TRuntimeContext() or
TImportContext() or
TCallContext(ControlFlowNode call, PointsToContext outerContext, int cost) {
total_cost(call, outerContext) = cost and
cost <= max_context_cost()
} or
TObjectContext(SelfInstanceInternal object)
/**
* A points-to context. Context can be one of:
* * "main": Used for scripts.
* * "import": Use for non-script modules.
* * "default": Use for functions and methods without caller context.
* * All other contexts are call contexts and consist of a pair of call-site and caller context.
*/
class PointsToContext extends TPointsToContext {
/** Gets a textual representation of this element. */
cached
string toString() {
this = TMainContext() and result = "main"
or
this = TRuntimeContext() and result = "runtime"
or
this = TImportContext() and result = "import"
or
exists(CallNode callsite, PointsToContext outerContext |
this = TCallContext(callsite, outerContext, _) and
result = callsite.getLocation() + " from " + outerContext.toString()
)
}
/** Holds if `call` is the call-site from which this context was entered and `outer` is the caller's context. */
predicate fromCall(CallNode call, PointsToContext caller) {
caller.appliesTo(call) and
this = TCallContext(call, caller, _)
}
/** Holds if `call` is the call-site from which this context was entered and `caller` is the caller's context. */
predicate fromCall(CallNode call, PythonFunctionObjectInternal callee, PointsToContext caller) {
call = callee.getACall(caller) and
this = TCallContext(call, caller, _)
}
/** Gets the caller context for this callee context. */
PointsToContext getOuter() { this = TCallContext(_, result, _) }
/** Holds if this context is relevant to the given scope. */
predicate appliesToScope(Scope s) {
/* Scripts */
this = TMainContext() and maybe_main(s)
or
/* Modules and classes evaluated at import */
s instanceof ImportTimeScope and this = TImportContext()
or
this = TRuntimeContext() and executes_in_runtime_context(s)
or
/* Called functions, regardless of their name */
exists(
PythonFunctionObjectInternal callable, ControlFlowNode call, TPointsToContext outerContext
|
call = callable.getACall(outerContext) and
this = TCallContext(call, outerContext, _)
|
s = callable.getScope()
)
or
InterProceduralPointsTo::callsite_calls_function(_, _, s, this, _)
}
/** Holds if this context can apply to the CFG node `n`. */
pragma[inline]
predicate appliesTo(ControlFlowNode n) {
exists(Scope s |
this.appliesToScope(pragma[only_bind_into](s)) and pragma[only_bind_into](s) = n.getScope()
)
}
/** Holds if this context is a call context. */
predicate isCall() { this = TCallContext(_, _, _) }
/** Holds if this is the "main" context. */
predicate isMain() { this = TMainContext() }
/** Holds if this is the "import" context. */
predicate isImport() { this = TImportContext() }
/** Holds if this is the "default" context. */
predicate isRuntime() { this = TRuntimeContext() }
/** Holds if this context or one of its caller contexts is the default context. */
predicate fromRuntime() {
this.isRuntime()
or
this.getOuter().fromRuntime()
}
/** Gets the depth (number of calls) for this context. */
int getDepth() {
not exists(this.getOuter()) and result = 0
or
result = this.getOuter().getDepth() + 1
}
int getCost() { result = context_cost(this) }
CallNode getCall() { this = TCallContext(result, _, _) }
/** Holds if a call would be too expensive to create a new context for */
pragma[nomagic]
predicate untrackableCall(CallNode call) { total_cost(call, this) > max_context_cost() }
CallNode getRootCall() {
this = TCallContext(result, TImportContext(), _)
or
result = this.getOuter().getRootCall()
}
/** Gets a version of Python that this context includes */
pragma[inline]
Version getAVersion() {
/* Currently contexts do not include any version information, but may do in the future */
result = major_version()
}
}
private predicate in_source(Scope s) { exists(s.getEnclosingModule().getFile().getRelativePath()) }
/**
* Holds if this scope can be executed in the default context.
* All modules and classes executed at import time and
* all "public" functions and methods, including those invoked by the VM.
*/
predicate executes_in_runtime_context(Function f) {
/* "Public" scope, i.e. functions whose name starts not with an underscore, or special methods */
(f.getName().charAt(0) != "_" or f.isSpecialMethod() or f.isInitMethod()) and
in_source(f)
}
private predicate maybe_main(Module m) {
exists(If i, Compare cmp, Name name, StringLiteral main | m.getAStmt() = i and i.getTest() = cmp |
cmp.compares(name, any(Eq eq), main) and
name.getId() = "__name__" and
main.getText() = "__main__"
)
}