Skip to content

Commit 64810f6

Browse files
committed
C#: Improve ConstantCondition.ql
1 parent 587901b commit 64810f6

File tree

2 files changed

+172
-3
lines changed

2 files changed

+172
-3
lines changed

csharp/ql/src/Bad Practices/Control-Flow/ConstantCondition.ql

Lines changed: 79 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,90 @@
1616
import csharp
1717
import semmle.code.csharp.commons.Assertions
1818
import semmle.code.csharp.commons.Constants
19+
import semmle.code.csharp.controlflow.BasicBlocks
20+
import semmle.code.csharp.controlflow.Guards as Guards
21+
import codeql.controlflow.queries.ConstantCondition as ConstCond
22+
23+
module ConstCondInput implements ConstCond::InputSig<ControlFlow::BasicBlock> {
24+
class SsaDefinition = Ssa::Definition;
25+
26+
class GuardValue = Guards::GuardValue;
27+
28+
class Guard = Guards::Guards::Guard;
29+
30+
predicate ssaControlsBranchEdge(SsaDefinition def, BasicBlock bb1, BasicBlock bb2, GuardValue v) {
31+
Guards::Guards::ssaControlsBranchEdge(def, bb1, bb2, v)
32+
}
33+
34+
import Guards::Guards::InternalUtil
35+
}
36+
37+
module ConstCondImpl = ConstCond::Make<Location, Cfg, ConstCondInput>;
38+
39+
predicate nullCheck(Expr e, boolean direct) {
40+
exists(QualifiableExpr qe | qe.isConditional() and qe.getQualifier() = e and direct = true)
41+
or
42+
exists(NullCoalescingExpr nce | nce.getLeftOperand() = e and direct = true)
43+
or
44+
exists(ConditionalExpr ce | ce.getThen() = e or ce.getElse() = e |
45+
nullCheck(ce, _) and direct = false
46+
)
47+
}
48+
49+
predicate constantGuard(
50+
Guards::Guards::Guard g, string msg, Guards::Guards::Guard reason, string reasonMsg
51+
) {
52+
ConstCondImpl::problems(g, msg, reason, reasonMsg) and
53+
// conditional qualified expressions sit at an akward place in the CFG, which
54+
// leads to FPs
55+
not g.(QualifiableExpr).getQualifier() = reason and
56+
// and if they're extension method calls, the syntactic qualifier is actually argument 0
57+
not g.(ExtensionMethodCall).getArgument(0) = reason and
58+
// if a logical connective is constant, one of its operands is constant, so
59+
// we report that instead
60+
not g instanceof LogicalNotExpr and
61+
not g instanceof LogicalAndExpr and
62+
not g instanceof LogicalOrExpr and
63+
// if a logical connective is a reason for another condition to be constant,
64+
// then one of its operands is a more precise reason
65+
not reason instanceof LogicalNotExpr and
66+
not reason instanceof LogicalAndExpr and
67+
not reason instanceof LogicalOrExpr and
68+
// don't report double-checked locking
69+
not exists(LockStmt ls, BasicBlock bb |
70+
bb = ls.getBasicBlock() and
71+
reason.getBasicBlock().strictlyDominates(bb) and
72+
bb.dominates(g.getBasicBlock())
73+
) and
74+
// exclude indirect null checks like `x` in `(b ? x : null)?.Foo()`
75+
not nullCheck(g, false)
76+
}
1977

2078
/** A constant condition. */
21-
abstract class ConstantCondition extends Expr {
79+
abstract class ConstantCondition extends Guards::Guards::Guard {
2280
/** Gets the alert message for this constant condition. */
2381
abstract string getMessage();
2482

83+
predicate hasReason(Guards::Guards::Guard reason, string reasonMsg) {
84+
// dummy value, overridden when message has a placeholder
85+
reason = this and reasonMsg = "dummy"
86+
}
87+
2588
/** Holds if this constant condition is white-listed. */
2689
predicate isWhiteListed() { none() }
2790
}
2891

92+
/** A constant guard. */
93+
class ConstantGuard extends ConstantCondition {
94+
ConstantGuard() { constantGuard(this, _, _, _) }
95+
96+
override string getMessage() { constantGuard(this, result, _, _) }
97+
98+
override predicate hasReason(Guards::Guards::Guard reason, string reasonMsg) {
99+
constantGuard(this, _, reason, reasonMsg)
100+
}
101+
}
102+
29103
/** A constant Boolean condition. */
30104
class ConstantBooleanCondition extends ConstantCondition {
31105
boolean b;
@@ -111,6 +185,7 @@ class ConstantMatchingCondition extends ConstantCondition {
111185
boolean b;
112186

113187
ConstantMatchingCondition() {
188+
this instanceof Expr and
114189
forex(ControlFlow::Node cfn | cfn = this.getAControlFlowNode() |
115190
exists(ControlFlow::MatchingSuccessor t | exists(cfn.getASuccessorByType(t)) |
116191
b = t.getValue()
@@ -138,9 +213,10 @@ class ConstantMatchingCondition extends ConstantCondition {
138213
}
139214
}
140215

141-
from ConstantCondition c, string msg
216+
from ConstantCondition c, string msg, Guards::Guards::Guard reason, string reasonMsg
142217
where
143218
msg = c.getMessage() and
219+
c.hasReason(reason, reasonMsg) and
144220
not c.isWhiteListed() and
145221
not isExprInAssertion(c)
146-
select c, msg
222+
select c, msg, reason, reasonMsg
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/**
2+
* Provides a query for detecting constant conditions.
3+
*/
4+
overlay[local?]
5+
module;
6+
7+
private import codeql.controlflow.BasicBlock as BB
8+
private import codeql.util.Location
9+
10+
private signature class TypSig;
11+
12+
signature module InputSig<TypSig BasicBlock> {
13+
class SsaDefinition;
14+
15+
/** An abstract value that a `Guard` may evaluate to. */
16+
class GuardValue {
17+
/** Gets a textual representation of this value. */
18+
string toString();
19+
20+
/**
21+
* Gets the dual value. Examples of dual values include:
22+
* - null vs. not null
23+
* - true vs. false
24+
* - evaluating to a specific value vs. evaluating to any other value
25+
* - throwing an exception vs. not throwing an exception
26+
*/
27+
GuardValue getDualValue();
28+
29+
/** Holds if this value represents throwing an exception. */
30+
predicate isThrowsException();
31+
}
32+
33+
/**
34+
* A guard. This may be any expression whose value determines subsequent
35+
* control flow. It may also be a switch case, which as a guard is considered
36+
* to evaluate to either true or false depending on whether the case matches.
37+
*/
38+
class Guard {
39+
/** Gets a textual representation of this guard. */
40+
string toString();
41+
42+
/**
43+
* Holds if this guard evaluating to `v` corresponds to taking the edge
44+
* from `bb1` to `bb2`. For ordinary conditional branching this guard is
45+
* the last node in `bb1`, but for switch case matching it is the switch
46+
* expression, which may either be in `bb1` or an earlier basic block.
47+
*/
48+
predicate hasValueBranchEdge(BasicBlock bb1, BasicBlock bb2, GuardValue v);
49+
}
50+
51+
/**
52+
* Holds if `def` evaluating to `v` controls the control-flow branch
53+
* edge from `bb1` to `bb2`. That is, following the edge from `bb1` to
54+
* `bb2` implies that `def` evaluated to `v`.
55+
*/
56+
predicate ssaControlsBranchEdge(SsaDefinition def, BasicBlock bb1, BasicBlock bb2, GuardValue v);
57+
58+
bindingset[gv1, gv2]
59+
predicate disjointValues(GuardValue gv1, GuardValue gv2);
60+
}
61+
62+
module Make<LocationSig Location, BB::CfgSig<Location> Cfg, InputSig<Cfg::BasicBlock> Input> {
63+
private import Cfg
64+
private import Input
65+
66+
private predicate ssaControlsGuardEdge(
67+
SsaDefinition def, GuardValue v, Guard g, BasicBlock bb1, BasicBlock bb2, GuardValue gv
68+
) {
69+
g.hasValueBranchEdge(bb1, bb2, gv) and
70+
ssaControlsBranchEdge(def, bb1, bb2, v)
71+
}
72+
73+
query predicate problems(Guard g, string msg, Guard reason, string reasonMsg) {
74+
exists(
75+
BasicBlock bb, GuardValue gv, GuardValue dual, SsaDefinition def, GuardValue v1,
76+
GuardValue v2, BasicBlock bb1, BasicBlock bb2
77+
|
78+
// If `g` in `bb` evaluates to `gv` then `def` has value `v1`,
79+
ssaControlsGuardEdge(def, v1, g, bb, _, gv) and
80+
dual = gv.getDualValue() and
81+
not gv.isThrowsException() and
82+
not dual.isThrowsException() and
83+
// but `def` controls `bb` with value `v2` via the guard `reason` taking the branch `bb1` to `bb2`,
84+
ssaControlsGuardEdge(def, v2, reason, bb1, bb2, _) and
85+
dominatingEdge(bb1, bb2) and
86+
bb2.dominates(bb) and
87+
// and `v1` and `v2` are disjoint so `g` cannot be `gv`.
88+
disjointValues(v1, v2) and
89+
msg = "Condition is always " + dual + " because of $@." and
90+
reasonMsg = reason.toString()
91+
)
92+
}
93+
}

0 commit comments

Comments
 (0)