Skip to content

Commit 055351a

Browse files
committed
C#: Update nullness analyses
Port the SSA-based logic from the Java nullness analyses.
1 parent d2a431e commit 055351a

26 files changed

+1484
-546
lines changed

csharp/ql/src/CSI/NullAlways.qhelp

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,39 @@
22
"-//Semmle//qhelp//EN"
33
"qhelp.dtd">
44
<qhelp>
5+
6+
57
<overview>
6-
<p>If a variable is dereferenced, and the variable has a null value on all possible execution paths
7-
leading to the dereferencing, it is guaranteed to result in a <code>NullReferenceException</code>.
8+
<p>If a variable is dereferenced, and the variable has a <code>null</code>
9+
value on all possible execution paths leading to the dereferencing, the dereferencing is
10+
guaranteed to result in a <code>NullReferenceException</code>.
811
</p>
912

1013
</overview>
1114
<recommendation>
12-
<p>Examine the code to check for possible errors.</p>
15+
16+
<p>Ensure that the variable does not have a <code>null</code> value when it is dereferenced.
17+
</p>
18+
1319
</recommendation>
20+
<example>
21+
<p>
22+
In the following examples, the condition <code>s.Length > 0</code> is only
23+
executed if <code>s</code> is <code>null</code>.
24+
</p>
25+
26+
<sample src="NullAlwaysBad.cs" />
27+
28+
<p>
29+
In the revised example, the condition is guarded correctly by using <code>&amp;&amp;</code> instead of
30+
<code>||</code>.
31+
</p>
32+
33+
<sample src="NullAlwaysGood.cs" />
34+
</example>
35+
<references>
36+
37+
<li>Microsoft, <a href="https://docs.microsoft.com/en-us/dotnet/api/system.nullreferenceexception">NullReferenceException Class</a>.</li>
38+
39+
</references>
1440
</qhelp>

csharp/ql/src/CSI/NullAlways.ql

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
/**
22
* @name Dereferenced variable is always null
3-
* @description Finds uses of a variable that may cause a NullPointerException
3+
* @description Dereferencing a variable whose value is 'null' causes a 'NullReferenceException'.
44
* @kind problem
55
* @problem.severity error
6-
* @precision medium
6+
* @precision very-high
77
* @id cs/dereferenced-value-is-always-null
88
* @tags reliability
99
* correctness
@@ -14,6 +14,6 @@
1414
import csharp
1515
import semmle.code.csharp.dataflow.Nullness
1616

17-
from VariableAccess access, LocalVariable var
18-
where access = unguardedNullDereference(var)
19-
select access, "Variable $@ is always null here.", var, var.getName()
17+
from Dereference d, Ssa::SourceVariable v
18+
where d.isFirstAlwaysNull(v)
19+
select d, "Variable '$@' is always null here.", v, v.toString()

csharp/ql/src/CSI/NullAlwaysBad.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
using System;
2+
3+
namespace NullAlways
4+
{
5+
class Bad
6+
{
7+
void DoPrint(string s)
8+
{
9+
if (s != null || s.Length > 0)
10+
Console.WriteLine(s);
11+
}
12+
}
13+
}

csharp/ql/src/CSI/NullAlwaysGood.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
using System;
2+
3+
namespace NullAlways
4+
{
5+
class Good
6+
{
7+
void DoPrint(string s)
8+
{
9+
if (s != null && s.Length > 0)
10+
Console.WriteLine(s);
11+
}
12+
}
13+
}

csharp/ql/src/CSI/NullMaybe.qhelp

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,41 @@
22
"-//Semmle//qhelp//EN"
33
"qhelp.dtd">
44
<qhelp>
5+
6+
57
<overview>
6-
<p>If a variable is dereferenced, and the variable may have a null value on some execution paths
7-
leading to the dereferencing, the dereferencing may result in a <code>NullReferenceException</code>.
8+
<p>If a variable is dereferenced, and the variable may have a <code>null</code>
9+
value on some execution paths leading to the dereferencing, the dereferencing
10+
may result in a <code>NullReferenceException</code>.
811
</p>
912

1013
</overview>
1114
<recommendation>
12-
<p>Examine the code to check for possible errors.</p>
15+
16+
<p>Ensure that the variable does not have a <code>null</code> value when it is dereferenced.
17+
</p>
18+
1319
</recommendation>
20+
<example>
21+
<p>
22+
In the following example, the method <code>DoPrint()</code> dereferences its parameter
23+
<code>o</code> unconditionally, resulting in a <code>NullReferenceException</code> via
24+
the call <code>DoPrint(null)</code>.
25+
</p>
26+
27+
<sample src="NullMaybeBad.cs" />
28+
29+
<p>
30+
In the revised example, the method <code>DoPrint()</code> guards the dereferencing with
31+
a <code>null</code> check.
32+
</p>
33+
34+
<sample src="NullMaybeGood.cs" />
35+
36+
</example>
37+
<references>
38+
39+
<li>Microsoft, <a href="https://docs.microsoft.com/en-us/dotnet/api/system.nullreferenceexception">NullReferenceException Class</a>.</li>
40+
41+
</references>
1442
</qhelp>

csharp/ql/src/CSI/NullMaybe.ql

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
/**
22
* @name Dereferenced variable may be null
3-
* @description Finds uses of a variable that may cause a NullPointerException
3+
* @description Dereferencing a variable whose value may be 'null' may cause a
4+
* 'NullReferenceException'.
45
* @kind problem
56
* @problem.severity warning
6-
* @precision medium
7+
* @precision high
78
* @id cs/dereferenced-value-may-be-null
89
* @tags reliability
910
* correctness
@@ -14,8 +15,6 @@
1415
import csharp
1516
import semmle.code.csharp.dataflow.Nullness
1617

17-
from VariableAccess access, LocalVariable var
18-
where access = unguardedMaybeNullDereference(var)
19-
// do not flag definite nulls here; these are already flagged by NullAlways.ql
20-
and not access = unguardedNullDereference(var)
21-
select access, "Variable $@ may be null here.", var, var.getName()
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"

csharp/ql/src/CSI/NullMaybeBad.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
using System;
2+
3+
class Bad
4+
{
5+
void DoPrint(object o)
6+
{
7+
Console.WriteLine(o.ToString());
8+
}
9+
10+
void M()
11+
{
12+
DoPrint("Hello");
13+
DoPrint(null);
14+
}
15+
}

csharp/ql/src/CSI/NullMaybeGood.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
using System;
2+
3+
class Good
4+
{
5+
void DoPrint(object o)
6+
{
7+
if (o != null)
8+
Console.WriteLine(o.ToString());
9+
}
10+
11+
void M()
12+
{
13+
DoPrint("Hello");
14+
DoPrint(null);
15+
}
16+
}

csharp/ql/src/semmle/code/csharp/controlflow/Guards.qll

Lines changed: 100 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -112,19 +112,28 @@ private import AbstractValues
112112
* an expression that may evaluate to `null`.
113113
*/
114114
class DereferenceableExpr extends Expr {
115+
private boolean isNullableType;
116+
115117
DereferenceableExpr() {
116118
exists(Expr e, Type t |
117119
// There is currently a bug in the extractor: the type of `x?.Length` is
118120
// incorrectly `int`, while it should have been `int?`. We apply
119121
// `getNullEquivParent()` as a workaround
120122
this = getNullEquivParent*(e) and
121123
t = e.getType() |
122-
t instanceof NullableType
124+
t instanceof NullableType and
125+
isNullableType = true
123126
or
124-
t instanceof RefType
127+
t instanceof RefType and
128+
isNullableType = false
125129
)
126130
}
127131

132+
/** Holds if this expression has a nullable type `T?`. */
133+
predicate hasNullableType() {
134+
isNullableType = true
135+
}
136+
128137
/**
129138
* Gets an expression that directly tests whether this expression is `null`.
130139
*
@@ -177,9 +186,22 @@ class DereferenceableExpr extends Expr {
177186
if ie.(IsConstantExpr).getConstant() instanceof NullLiteral then
178187
// E.g. `x is null`
179188
isNull = branch
180-
else
189+
else (
181190
// E.g. `x is string` or `x is ""`
182-
(branch = true and isNull = false)
191+
branch = true and isNull = false
192+
or
193+
// E.g. `x is string` where `x` has type `string`
194+
ie = any(IsTypeExpr ite | ite.getCheckedType() = ite.getExpr().getType()) and
195+
branch = false and
196+
isNull = true
197+
)
198+
)
199+
or
200+
this.hasNullableType() and
201+
result = any(PropertyAccess pa |
202+
pa.getQualifier() = this and
203+
pa.getTarget().hasName("HasValue") and
204+
if branch = true then isNull = false else isNull = true
183205
)
184206
or
185207
isCustomNullCheck(result, this, v, isNull)
@@ -307,19 +329,23 @@ class AccessOrCallExpr extends Expr {
307329
}
308330

309331
private Declaration getDeclarationTarget(Expr e) {
310-
e = any(AssignableRead ar | result = ar.getTarget()) or
332+
e = any(AssignableAccess aa | result = aa.getTarget()) or
311333
result = e.(Call).getTarget()
312334
}
313335

314336
private Ssa::Definition getAnSsaQualifier(Expr e) {
315-
e = getATrackedRead(result)
337+
e = getATrackedAccess(result)
316338
or
317-
not e = getATrackedRead(_) and
339+
not e = getATrackedAccess(_) and
318340
result = getAnSsaQualifier(e.(QualifiableExpr).getQualifier())
319341
}
320342

321-
private AssignableRead getATrackedRead(Ssa::Definition def) {
322-
result = def.getARead() and
343+
private AssignableAccess getATrackedAccess(Ssa::Definition def) {
344+
(
345+
result = def.getARead()
346+
or
347+
result = def.(Ssa::ExplicitDefinition).getADefinition().getTargetAccess()
348+
) and
323349
not def instanceof Ssa::ImplicitUntrackedDefinition
324350
}
325351

@@ -384,6 +410,15 @@ class GuardedExpr extends AccessOrCallExpr {
384410
v = v0
385411
}
386412

413+
/**
414+
* Holds if this expression must have abstract value `v`. That is, this
415+
* expression is guarded by a structurally equal expression having abstract
416+
* value `v`.
417+
*/
418+
predicate mustHaveValue(AbstractValue v) {
419+
exists(Expr e | e = this.getAGuard(e, v))
420+
}
421+
387422
/**
388423
* Holds if this expression is guarded by expression `cond`, which must
389424
* evaluate to `b`. The expression `sub` is a sub expression of `cond`
@@ -401,7 +436,7 @@ class GuardedExpr extends AccessOrCallExpr {
401436
/** An expression guarded by a `null` check. */
402437
class NullGuardedExpr extends GuardedExpr {
403438
NullGuardedExpr() {
404-
exists(Expr e, NullValue v | e = this.getAGuard(e, v) | not v.isNull())
439+
this.mustHaveValue(any(NullValue v | not v.isNull()))
405440
}
406441
}
407442

@@ -420,11 +455,28 @@ module Internal {
420455

421456
/** Holds if expression `e` is a non-`null` value. */
422457
predicate nonNullValue(Expr e) {
423-
e.stripCasts() = any(Expr s | s.hasValue() and not s instanceof NullLiteral)
458+
e instanceof ObjectCreation
459+
or
460+
e instanceof ArrayCreation
461+
or
462+
e.hasValue() and
463+
not e instanceof NullLiteral
464+
or
465+
e instanceof ThisAccess
466+
or
467+
e instanceof AddExpr and
468+
e.getType() instanceof StringType
469+
}
470+
471+
/** Holds if expression `e2` is a non-`null` value whenever `e1` is. */
472+
predicate nonNullValueImplied(Expr e1, Expr e2) {
473+
e1 = e2.(CastExpr).getExpr()
474+
or
475+
e1 = e2.(AssignExpr).getRValue()
424476
}
425477

426478
/**
427-
* Gets the parent expression of `e` which is `null` only if `e` is `null`,
479+
* Gets the parent expression of `e` which is `null` iff `e` is `null`,
428480
* if any. For example, `result = x?.y` and `e = x`, or `result = x + 1`
429481
* and `e = x`.
430482
*/
@@ -433,6 +485,8 @@ module Internal {
433485
qe.getQualifier() = e and
434486
qe.isConditional() and
435487
(
488+
// The accessed declaration must have a value type in order
489+
// for `only if` to hold
436490
result.(FieldAccess).getTarget().getType() instanceof ValueType
437491
or
438492
result.(Call).getTarget().getReturnType() instanceof ValueType
@@ -444,11 +498,28 @@ module Internal {
444498
result = bao and
445499
bao.getAnOperand() = e and
446500
bao.getAnOperand() = o and
501+
// The other operand must be provably non-null in order
502+
// for `only if` to hold
447503
nonNullValue(o) and
448504
e != o
449505
)
450506
}
451507

508+
/**
509+
* Gets a child expression of `e` which is `null` only if `e` is `null`.
510+
*/
511+
Expr getANullImplyingChild(Expr e) {
512+
e = any(QualifiableExpr qe |
513+
qe.isConditional() and
514+
result = qe.getQualifier()
515+
)
516+
or
517+
// In C#, `null + 1` has type `int?` with value `null`
518+
e = any(BinaryArithmeticOperation bao |
519+
result = bao.getAnOperand()
520+
)
521+
}
522+
452523
/** An expression whose value may control the execution of another element. */
453524
class Guard extends Expr {
454525
private AbstractValue val;
@@ -785,6 +856,23 @@ module Internal {
785856
v1 instanceof NullValue and
786857
v2 = v1
787858
or
859+
g1 instanceof DereferenceableExpr and
860+
g2 = getANullImplyingChild(g1) and
861+
v1 = any(NullValue nv | not nv.isNull()) and
862+
v2 = v1
863+
or
864+
g2 = g1.(AssignExpr).getRValue() and
865+
v1 = g1.getAValue() and
866+
v2 = v1
867+
or
868+
g2 = g1.(Assignment).getLValue() and
869+
v1 = g1.getAValue() and
870+
v2 = v1
871+
or
872+
g2 = g1.(CastExpr).getExpr() and
873+
v1 = g1.getAValue() and
874+
v2 = v1.(NullValue)
875+
or
788876
exists(PreSsa::Definition def |
789877
def.getDefinition().getSource() = g2 |
790878
g1 = def.getARead() and

0 commit comments

Comments
 (0)