Skip to content

Commit 91e4f7a

Browse files
committed
C#: Make cs/dereferenced-value-may-be-null a path query
1 parent e2f271b commit 91e4f7a

File tree

4 files changed

+972
-120
lines changed

4 files changed

+972
-120
lines changed

csharp/ql/src/CSI/NullMaybe.ql

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
* @name Dereferenced variable may be null
33
* @description Dereferencing a variable whose value may be 'null' may cause a
44
* 'NullReferenceException'.
5-
* @kind problem
5+
* @kind path-problem
66
* @problem.severity warning
77
* @precision high
88
* @id cs/dereferenced-value-may-be-null
@@ -14,7 +14,8 @@
1414

1515
import csharp
1616
import semmle.code.csharp.dataflow.Nullness
17+
import PathGraph
1718

18-
from Dereference d, Ssa::SourceVariable v, string msg, Element reason
19-
where d.isFirstMaybeNull(v.getAnSsaDefinition(), msg, reason)
20-
select d, "Variable $@ may be null here " + msg + ".", v, v.toString(), reason, "this"
19+
from Dereference d, PathNode source, PathNode sink, Ssa::SourceVariable v, string msg, Element reason
20+
where d.isFirstMaybeNull(v.getAnSsaDefinition(), source, sink, msg, reason)
21+
select d, source, sink, "Variable $@ may be null here " + msg + ".", v, v.toString(), reason, "this"

csharp/ql/src/semmle/code/csharp/dataflow/Nullness.qll

Lines changed: 185 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,15 @@ private ControlFlowElement getANullCheck(Ssa::Definition def, SuccessorTypes::Co
147147
)
148148
}
149149

150+
private predicate isMaybeNullArgument(Ssa::ExplicitDefinition def, MaybeNullExpr arg) {
151+
exists(AssignableDefinitions::ImplicitParameterDefinition pdef, Parameter p |
152+
pdef = def.getADefinition() |
153+
p = pdef.getParameter().getSourceDeclaration() and
154+
p.getAnAssignedArgument() = arg and
155+
not arg.getEnclosingCallable().getEnclosingCallable*() instanceof TestMethod
156+
)
157+
}
158+
150159
/** Holds if `def` is an SSA definition that may be `null`. */
151160
private predicate defMaybeNull(Ssa::Definition def, string msg, Element reason) {
152161
// A variable compared to `null` might be `null`
@@ -160,18 +169,14 @@ private predicate defMaybeNull(Ssa::Definition def, string msg, Element reason)
160169
) = 1 and
161170
not nonNullDef(def) and
162171
// Don't use a check as reason if there is a `null` assignment
163-
not def.(Ssa::ExplicitDefinition).getADefinition().getSource() instanceof MaybeNullExpr
172+
// or argument
173+
not def.(Ssa::ExplicitDefinition).getADefinition().getSource() instanceof MaybeNullExpr and
174+
not isMaybeNullArgument(def, _)
164175
)
165176
or
166177
// A parameter might be `null` if there is a `null` argument somewhere
167-
exists(AssignableDefinitions::ImplicitParameterDefinition pdef, Parameter p, MaybeNullExpr arg |
168-
pdef = def.(Ssa::ExplicitDefinition).getADefinition() |
169-
p = pdef.getParameter().getSourceDeclaration() and
170-
p.getAnAssignedArgument() = arg and
171-
reason = arg and
172-
msg = "because of $@ null argument" and
173-
not arg.getEnclosingCallable().getEnclosingCallable*() instanceof TestMethod
174-
)
178+
isMaybeNullArgument(def, reason) and
179+
msg = "because of $@ null argument"
175180
or
176181
// If the source of a variable is `null` then the variable may be `null`
177182
exists(AssignableDefinition adef |
@@ -202,12 +207,12 @@ private predicate defNullImpliesStep(Ssa::Definition def1, BasicBlock bb1, Ssa::
202207
def1.getSourceVariable() = v
203208
|
204209
def2.(Ssa::PseudoDefinition).getAnInput() = def1 and
205-
def2.definesAt(bb2, _)
210+
bb2 = def2.getBasicBlock()
206211
or
207212
def2 = def1 and
208213
not exists(Ssa::PseudoDefinition def |
209214
def.getSourceVariable() = v and
210-
def.definesAt(bb2, _)
215+
bb2 = def.getBasicBlock()
211216
)
212217
) and
213218
def1.isLiveAtEndOfBlock(bb1) and
@@ -224,13 +229,12 @@ private predicate defNullImpliesStep(Ssa::Definition def1, BasicBlock bb1, Ssa::
224229
* The transitive closure of `defNullImpliesStep()` originating from `defMaybeNull()`.
225230
* That is, those basic blocks for which the SSA definition is suspected of being `null`.
226231
*/
227-
private predicate defMaybeNullInBlock(Ssa::Definition def, Ssa::SourceVariable v, BasicBlock bb) {
232+
private predicate defMaybeNullInBlock(Ssa::Definition def, BasicBlock bb) {
228233
defMaybeNull(def, _, _) and
229-
def.definesAt(bb, _) and
230-
v = def.getSourceVariable()
234+
bb = def.getBasicBlock()
231235
or
232236
exists(BasicBlock mid, Ssa::Definition midDef |
233-
defMaybeNullInBlock(midDef, v, mid) |
237+
defMaybeNullInBlock(midDef, mid) |
234238
defNullImpliesStep(midDef, mid, def, bb)
235239
)
236240
}
@@ -242,20 +246,167 @@ private predicate defMaybeNullInBlock(Ssa::Definition def, Ssa::SourceVariable v
242246
private predicate nullDerefCandidateVariable(Ssa::SourceVariable v) {
243247
exists(Ssa::Definition def, BasicBlock bb |
244248
potentialNullDereferenceAt(bb, _, def, _) |
245-
defMaybeNullInBlock(def, v, bb)
249+
defMaybeNullInBlock(def, bb) and
250+
v = def.getSourceVariable()
246251
)
247252
}
248253

249-
private predicate defMaybeNullInBlockOrigin(Ssa::Definition origin, Ssa::Definition def, BasicBlock bb) {
250-
nullDerefCandidateVariable(def.getSourceVariable()) and
251-
defMaybeNull(def, _, _) and
252-
def.definesAt(bb, _) and
253-
origin = def
254+
private predicate succStep(PathNode pred, Ssa::Definition def, BasicBlock bb) {
255+
defNullImpliesStep(pred.getSsaDefinition(), pred.getBasicBlock(), def, bb)
256+
}
257+
258+
private predicate succNullArgument(SourcePathNode pred, Ssa::Definition def, BasicBlock bb) {
259+
pred = TSourcePathNode(def, _, _, true) and
260+
bb = def.getBasicBlock()
261+
}
262+
263+
private predicate succSourceSink(SourcePathNode source, Ssa::Definition def, BasicBlock bb) {
264+
source = TSourcePathNode(def, _, _, false) and
265+
bb = def.getBasicBlock()
266+
}
267+
268+
private newtype TPathNode =
269+
TSourcePathNode(Ssa::Definition def, string msg, Element reason, boolean isNullArgument) {
270+
nullDerefCandidateVariable(def.getSourceVariable()) and
271+
defMaybeNull(def, msg, reason) and
272+
if isMaybeNullArgument(def, reason) then isNullArgument = true else isNullArgument = false
273+
}
254274
or
255-
exists(BasicBlock mid, Ssa::Definition midDef |
256-
defMaybeNullInBlockOrigin(origin, midDef, mid) and
257-
defNullImpliesStep(midDef, mid, def, bb)
258-
)
275+
TInternalPathNode(Ssa::Definition def, BasicBlock bb) {
276+
succStep(_, def, bb)
277+
or
278+
succNullArgument(_, def, bb)
279+
}
280+
or
281+
TSinkPathNode(Ssa::Definition def, BasicBlock bb, int i, Dereference d) {
282+
potentialNullDereferenceAt(bb, i, def, d) and
283+
(
284+
succStep(_, def, bb)
285+
or
286+
succNullArgument(_, def, bb)
287+
or
288+
succSourceSink(_, def, bb)
289+
)
290+
}
291+
292+
/**
293+
* An SSA definition, which may be `null`, augmented with at basic block which can
294+
* be reached without passing through a `null` check.
295+
*/
296+
abstract class PathNode extends TPathNode {
297+
/** Gets the SSA definition. */
298+
abstract Ssa::Definition getSsaDefinition();
299+
300+
/** Gets the basic block that can be reached without passing through a `null` check. */
301+
abstract BasicBlock getBasicBlock();
302+
303+
/** Gets another node that can be reached from this node. */
304+
abstract PathNode getASuccessor();
305+
306+
/** Gets a textual representation of this node. */
307+
abstract string toString();
308+
309+
/** Gets the location of this node. */
310+
abstract Location getLocation();
311+
}
312+
313+
private class SourcePathNode extends PathNode, TSourcePathNode {
314+
private Ssa::Definition def;
315+
private string msg;
316+
private Element reason;
317+
private boolean isNullArgument;
318+
319+
SourcePathNode() { this = TSourcePathNode(def, msg, reason, isNullArgument) }
320+
321+
override Ssa::Definition getSsaDefinition() { result = def }
322+
323+
override BasicBlock getBasicBlock() {
324+
isNullArgument = false and
325+
result = def.getBasicBlock()
326+
}
327+
328+
string getMessage() { result = msg }
329+
330+
Element getReason() { result = reason }
331+
332+
override PathNode getASuccessor() {
333+
succStep(this, result.getSsaDefinition(), result.getBasicBlock())
334+
or
335+
succNullArgument(this, result.getSsaDefinition(), result.getBasicBlock())
336+
or
337+
result instanceof SinkPathNode and
338+
succSourceSink(this, result.getSsaDefinition(), result.getBasicBlock())
339+
}
340+
341+
override string toString() {
342+
if isNullArgument = true then
343+
result = reason.toString()
344+
else
345+
result = def.toString()
346+
}
347+
348+
override Location getLocation() {
349+
if isNullArgument = true then
350+
result = reason.getLocation()
351+
else
352+
result = def.getLocation()
353+
}
354+
}
355+
356+
private class InternalPathNode extends PathNode, TInternalPathNode {
357+
private Ssa::Definition def;
358+
private BasicBlock bb;
359+
360+
InternalPathNode() { this = TInternalPathNode(def, bb) }
361+
362+
override Ssa::Definition getSsaDefinition() { result = def }
363+
364+
override BasicBlock getBasicBlock() { result = bb }
365+
366+
override PathNode getASuccessor() {
367+
succStep(this, result.getSsaDefinition(), result.getBasicBlock())
368+
}
369+
370+
override string toString() { result = bb.getFirstNode().toString() }
371+
372+
override Location getLocation() { result = bb.getFirstNode().getLocation() }
373+
}
374+
375+
private class SinkPathNode extends PathNode, TSinkPathNode {
376+
private Ssa::Definition def;
377+
private BasicBlock bb;
378+
private int i;
379+
private Dereference d;
380+
381+
SinkPathNode() { this = TSinkPathNode(def, bb, i, d) }
382+
383+
override Ssa::Definition getSsaDefinition() { result = def }
384+
385+
override BasicBlock getBasicBlock() { result = bb }
386+
387+
override PathNode getASuccessor() { none() }
388+
389+
Dereference getDereference() { result = d }
390+
391+
override string toString() { result = d.toString() }
392+
393+
override Location getLocation() { result = d.getLocation() }
394+
}
395+
396+
/**
397+
* Provides the query predicates needed to include a graph in a path-problem query
398+
* for `Dereference::is[First]MaybeNull()`.
399+
*/
400+
module PathGraph {
401+
query predicate nodes(PathNode n) {
402+
n.getASuccessor*() instanceof SinkPathNode
403+
}
404+
405+
query predicate edges(PathNode pred, PathNode succ) {
406+
nodes(pred) and
407+
nodes(succ) and
408+
succ = pred.getASuccessor()
409+
}
259410
}
260411

261412
private Ssa::Definition getAPseudoInput(Ssa::Definition def) {
@@ -282,7 +433,7 @@ private predicate defReaches(Ssa::Definition def, AssignableRead ar, boolean alw
282433
defReaches(def, mid, always) |
283434
ar = mid.getANextRead() and
284435
not mid = any(Dereference d |
285-
if always = true then d.isAlwaysNull(def.getSourceVariable()) else d.isMaybeNull(def, _, _)
436+
if always = true then d.isAlwaysNull(def.getSourceVariable()) else d.isMaybeNull(def, _, _, _, _)
286437
)
287438
)
288439
}
@@ -378,24 +529,16 @@ class Dereference extends G::DereferenceableExpr {
378529
defReaches(v.getAnSsaDefinition(), this, true)
379530
}
380531

381-
pragma[noinline]
382-
private predicate nullDerefCandidate(Ssa::Definition origin) {
383-
exists(Ssa::Definition ssa, BasicBlock bb |
384-
potentialNullDereferenceAt(bb, _, ssa, this) |
385-
defMaybeNullInBlockOrigin(origin, ssa, bb)
386-
)
387-
}
388-
389532
/**
390533
* Holds if this expression dereferences SSA definition `def`, which may
391534
* be `null`.
392535
*/
393-
predicate isMaybeNull(Ssa::Definition def, string msg, Element reason) {
394-
exists(Ssa::Definition origin, BasicBlock bb |
395-
this.nullDerefCandidate(origin) and
396-
defMaybeNull(origin, msg, reason) and
397-
potentialNullDereferenceAt(bb, _, def, this)
398-
) and
536+
predicate isMaybeNull(Ssa::Definition def, SourcePathNode source, SinkPathNode sink, string msg, Element reason) {
537+
source.getASuccessor*() = sink and
538+
msg = source.getMessage() and
539+
reason = source.getReason() and
540+
def = sink.getSsaDefinition() and
541+
this = sink.getDereference() and
399542
not this.isAlwaysNull(def.getSourceVariable())
400543
}
401544

@@ -404,8 +547,8 @@ class Dereference extends G::DereferenceableExpr {
404547
* be `null`, and this expression can be reached from `def` without passing
405548
* through another such dereference.
406549
*/
407-
predicate isFirstMaybeNull(Ssa::Definition def, string msg, Element reason) {
408-
this.isMaybeNull(def, msg, reason) and
550+
predicate isFirstMaybeNull(Ssa::Definition def, SourcePathNode source, SinkPathNode sink, string msg, Element reason) {
551+
this.isMaybeNull(def, source, sink, msg, reason) and
409552
defReaches(def, this, false)
410553
}
411554
}

csharp/ql/src/semmle/code/csharp/dataflow/SSA.qll

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2249,6 +2249,9 @@ module Ssa {
22492249
definesAt(this, bb, i, _)
22502250
}
22512251

2252+
/** Gets the basic block to which this SSA definition belongs. */
2253+
BasicBlock getBasicBlock() { this.definesAt(result, _) }
2254+
22522255
/**
22532256
* Holds if this SSA definition assigns to `out`/`ref` parameter `p`, and the
22542257
* parameter may remain unchanged throughout the rest of the enclosing callable.

0 commit comments

Comments
 (0)