Skip to content

Commit 2fea704

Browse files
committed
C#: Remove FPs from cs/dereferenced-value-may-be-null
Apply a conservative approach by filtering out results for accesses to captured nullable values, when there is an (implicit) call to the capturing callable which is `null`-guarded. For example: ``` bool M(int? i, IEnumerable<int> @is) { if (i.HasValue) return @is.Any(j => j == i.Value); // GOOD return false; } ```
1 parent a026b51 commit 2fea704

4 files changed

Lines changed: 58 additions & 38 deletions

File tree

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

Lines changed: 42 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,18 @@ class Guard extends Expr {
3131
predicate controlsNode(ControlFlow::Nodes::ElementNode cfn, AccessOrCallExpr sub, AbstractValue v) {
3232
isGuardedByNode(cfn, this, sub, v)
3333
}
34+
35+
/**
36+
* Holds if `cfn` is guarded by this expression having value `v`.
37+
*
38+
* Note: This predicate is inlined.
39+
*/
40+
pragma[inline]
41+
predicate controlsNode(ControlFlow::Nodes::ElementNode cfn, AbstractValue v) {
42+
guardControls(this, cfn.getBasicBlock(), v)
43+
or
44+
guardAssertionControlsNode(this, cfn, v)
45+
}
3446
}
3547

3648
/** An abstract value. */
@@ -920,34 +932,6 @@ module Internal {
920932
e = any(BinaryArithmeticOperation bao | result = bao.getAnOperand())
921933
}
922934

923-
/** Holds if basic block `bb` only is reached when guard `g` has abstract value `v`. */
924-
private predicate guardControls(Guard g, BasicBlock bb, AbstractValue v) {
925-
exists(ControlFlowElement cfe, ConditionalSuccessor s, AbstractValue v0, Guard g0 |
926-
cfe.controlsBlock(bb, s)
927-
|
928-
v0.branch(cfe, s, g0) and
929-
impliesSteps(g0, v0, g, v)
930-
)
931-
}
932-
933-
/**
934-
* Holds if control flow node `cfn` only is reached when guard `g` evaluates to `v`,
935-
* because of an assertion.
936-
*/
937-
private predicate guardAssertionControlsNode(Guard g, ControlFlow::Node cfn, AbstractValue v) {
938-
exists(Assertion a, Guard g0, AbstractValue v0 |
939-
asserts(a, g0, v0) and
940-
impliesSteps(g0, v0, g, v)
941-
|
942-
a.strictlyDominates(cfn.getBasicBlock())
943-
or
944-
exists(BasicBlock bb, int i, int j | bb.getNode(i) = a.getAControlFlowNode() |
945-
bb.getNode(j) = cfn and
946-
j > i
947-
)
948-
)
949-
}
950-
951935
/**
952936
* Holds if control flow element `cfe` only is reached when guard `g` evaluates to `v`,
953937
* because of an assertion.
@@ -1685,6 +1669,36 @@ module Internal {
16851669
adjacentReadPairSameVarUniquePredecessor(mc.getQualifier(), e)
16861670
)
16871671
}
1672+
1673+
/** Holds if basic block `bb` only is reached when guard `g` has abstract value `v`. */
1674+
cached
1675+
predicate guardControls(Guard g, BasicBlock bb, AbstractValue v) {
1676+
exists(ControlFlowElement cfe, ConditionalSuccessor s, AbstractValue v0, Guard g0 |
1677+
cfe.controlsBlock(bb, s)
1678+
|
1679+
v0.branch(cfe, s, g0) and
1680+
impliesSteps(g0, v0, g, v)
1681+
)
1682+
}
1683+
1684+
/**
1685+
* Holds if control flow node `cfn` only is reached when guard `g` evaluates to `v`,
1686+
* because of an assertion.
1687+
*/
1688+
cached
1689+
predicate guardAssertionControlsNode(Guard g, ControlFlow::Node cfn, AbstractValue v) {
1690+
exists(Assertion a, Guard g0, AbstractValue v0 |
1691+
asserts(a, g0, v0) and
1692+
impliesSteps(g0, v0, g, v)
1693+
|
1694+
a.strictlyDominates(cfn.getBasicBlock())
1695+
or
1696+
exists(BasicBlock bb, int i, int j | bb.getNode(i) = a.getAControlFlowNode() |
1697+
bb.getNode(j) = cfn and
1698+
j > i
1699+
)
1700+
)
1701+
}
16881702
}
16891703

16901704
import CachedWithCFG

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,19 @@ private predicate isMaybeNullArgument(Ssa::ExplicitDefinition def, MaybeNullExpr
181181
)
182182
}
183183

184+
/**
185+
* Holds if `edef` is an implicit entry definition for a captured variable that
186+
* may be guarded, because a call to the capturing callable is guarded.
187+
*/
188+
private predicate isMaybeGuardedCapturedDef(Ssa::ImplicitEntryDefinition edef) {
189+
exists(Ssa::ExplicitDefinition def, ControlFlow::Nodes::ElementNode c, G::Guard g, NullValue nv |
190+
def.isCapturedVariableDefinitionFlowIn(edef, c, _) and
191+
g = def.getARead() and
192+
g.controlsNode(c, nv) and
193+
nv.isNonNull()
194+
)
195+
}
196+
184197
/** Holds if `def` is an SSA definition that may be `null`. */
185198
private predicate defMaybeNull(Ssa::Definition def, string msg, Element reason) {
186199
// A variable compared to `null` might be `null`
@@ -215,6 +228,7 @@ private predicate defMaybeNull(Ssa::Definition def, string msg, Element reason)
215228
exists(Dereference d | dereferenceAt(_, _, def, d) |
216229
d.hasNullableType() and
217230
not def instanceof Ssa::PseudoDefinition and
231+
not isMaybeGuardedCapturedDef(def) and
218232
reason = def.getSourceVariable().getAssignable() and
219233
msg = "because it has a nullable type"
220234
)

csharp/ql/test/query-tests/Nullness/E.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -371,14 +371,14 @@ static bool Ex34(int? i, IEnumerable<int> @is)
371371
static bool Ex35(int? i, IEnumerable<int> @is)
372372
{
373373
if (i.HasValue)
374-
return @is.Any(j => j == i.Value); // GOOD (FALSE POSITIVE)
374+
return @is.Any(j => j == i.Value); // GOOD
375375
return false;
376376
}
377377

378378
static bool Ex36(int? i, IEnumerable<int> @is)
379379
{
380380
if (i.HasValue)
381-
@is = @is.Where(j => j == i.Value); // BAD (always)
381+
@is = @is.Where(j => j == i.Value); // BAD (always, false negative)
382382
i = null;
383383
return @is.Any();
384384
}

csharp/ql/test/query-tests/Nullness/NullMaybe.expected

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -353,10 +353,6 @@ nodes
353353
| E.cs:349:9:349:9 | access to local variable x |
354354
| E.cs:368:24:368:40 | SSA capture def(i) |
355355
| E.cs:368:34:368:34 | access to parameter i |
356-
| E.cs:374:28:374:44 | SSA capture def(i) |
357-
| E.cs:374:38:374:38 | access to parameter i |
358-
| E.cs:381:29:381:45 | SSA capture def(i) |
359-
| E.cs:381:39:381:39 | access to parameter i |
360356
| Forwarding.cs:7:16:7:23 | SSA def(s) |
361357
| Forwarding.cs:14:9:17:9 | if (...) ... |
362358
| Forwarding.cs:19:9:22:9 | if (...) ... |
@@ -689,8 +685,6 @@ edges
689685
| E.cs:342:13:342:32 | SSA def(x) | E.cs:343:9:343:9 | access to local variable x |
690686
| E.cs:348:17:348:36 | SSA def(x) | E.cs:349:9:349:9 | access to local variable x |
691687
| E.cs:368:24:368:40 | SSA capture def(i) | E.cs:368:34:368:34 | access to parameter i |
692-
| E.cs:374:28:374:44 | SSA capture def(i) | E.cs:374:38:374:38 | access to parameter i |
693-
| E.cs:381:29:381:45 | SSA capture def(i) | E.cs:381:39:381:39 | access to parameter i |
694688
| Forwarding.cs:7:16:7:23 | SSA def(s) | Forwarding.cs:14:9:17:9 | if (...) ... |
695689
| Forwarding.cs:14:9:17:9 | if (...) ... | Forwarding.cs:19:9:22:9 | if (...) ... |
696690
| Forwarding.cs:19:9:22:9 | if (...) ... | Forwarding.cs:24:9:27:9 | if (...) ... |
@@ -792,8 +786,6 @@ edges
792786
| E.cs:343:9:343:9 | access to local variable x | E.cs:342:13:342:32 | SSA def(x) | E.cs:343:9:343:9 | access to local variable x | Variable $@ may be null here because of $@ assignment. | E.cs:342:13:342:13 | x | x | E.cs:342:13:342:32 | String x = ... | this |
793787
| E.cs:349:9:349:9 | access to local variable x | E.cs:348:17:348:36 | SSA def(x) | E.cs:349:9:349:9 | access to local variable x | Variable $@ may be null here because of $@ assignment. | E.cs:348:17:348:17 | x | x | E.cs:348:17:348:36 | dynamic x = ... | this |
794788
| E.cs:368:34:368:34 | access to parameter i | E.cs:368:24:368:40 | SSA capture def(i) | E.cs:368:34:368:34 | access to parameter i | Variable $@ may be null here because it has a nullable type. | E.cs:366:27:366:27 | i | i | E.cs:366:27:366:27 | i | this |
795-
| E.cs:374:38:374:38 | access to parameter i | E.cs:374:28:374:44 | SSA capture def(i) | E.cs:374:38:374:38 | access to parameter i | Variable $@ may be null here because it has a nullable type. | E.cs:371:27:371:27 | i | i | E.cs:371:27:371:27 | i | this |
796-
| E.cs:381:39:381:39 | access to parameter i | E.cs:381:29:381:45 | SSA capture def(i) | E.cs:381:39:381:39 | access to parameter i | Variable $@ may be null here because it has a nullable type. | E.cs:378:27:378:27 | i | i | E.cs:378:27:378:27 | i | this |
797789
| GuardedString.cs:35:31:35:31 | access to local variable s | GuardedString.cs:7:16:7:32 | SSA def(s) | GuardedString.cs:35:31:35:31 | access to local variable s | Variable $@ may be null here because of $@ assignment. | GuardedString.cs:7:16:7:16 | s | s | GuardedString.cs:7:16:7:32 | String s = ... | this |
798790
| NullMaybeBad.cs:7:27:7:27 | access to parameter o | NullMaybeBad.cs:13:17:13:20 | null | NullMaybeBad.cs:7:27:7:27 | access to parameter o | Variable $@ may be null here because of $@ null argument. | NullMaybeBad.cs:5:25:5:25 | o | o | NullMaybeBad.cs:13:17:13:20 | null | this |
799791
| StringConcatenation.cs:16:17:16:17 | access to local variable s | StringConcatenation.cs:14:16:14:23 | SSA def(s) | StringConcatenation.cs:16:17:16:17 | access to local variable s | Variable $@ may be null here because of $@ assignment. | StringConcatenation.cs:14:16:14:16 | s | s | StringConcatenation.cs:14:16:14:23 | String s = ... | this |

0 commit comments

Comments
 (0)