From a374540c55d90bbbbb34ff694b63b2a7f5d2b0a5 Mon Sep 17 00:00:00 2001 From: Asger F Date: Wed, 12 Sep 2018 18:37:21 +0100 Subject: [PATCH 01/27] JS: Range analysis library --- javascript/ql/src/javascript.qll | 1 + .../src/semmle/javascript/RangeAnalysis.qll | 366 ++++++++++++++++++ .../RangeAnalysis/DeadBranch.expected | 0 .../library-tests/RangeAnalysis/DeadBranch.ql | 35 ++ .../test/library-tests/RangeAnalysis/plus.js | 28 ++ .../test/library-tests/RangeAnalysis/tst.js | 52 +++ 6 files changed, 482 insertions(+) create mode 100644 javascript/ql/src/semmle/javascript/RangeAnalysis.qll create mode 100644 javascript/ql/test/library-tests/RangeAnalysis/DeadBranch.expected create mode 100644 javascript/ql/test/library-tests/RangeAnalysis/DeadBranch.ql create mode 100644 javascript/ql/test/library-tests/RangeAnalysis/plus.js create mode 100644 javascript/ql/test/library-tests/RangeAnalysis/tst.js diff --git a/javascript/ql/src/javascript.qll b/javascript/ql/src/javascript.qll index 030bc3ce33d3..4202d86ce1ea 100644 --- a/javascript/ql/src/javascript.qll +++ b/javascript/ql/src/javascript.qll @@ -36,6 +36,7 @@ import semmle.javascript.NPM import semmle.javascript.Paths import semmle.javascript.Promises import semmle.javascript.CanonicalNames +import semmle.javascript.RangeAnalysis import semmle.javascript.Regexp import semmle.javascript.SSA import semmle.javascript.StandardLibrary diff --git a/javascript/ql/src/semmle/javascript/RangeAnalysis.qll b/javascript/ql/src/semmle/javascript/RangeAnalysis.qll new file mode 100644 index 000000000000..c4c8a287bc5a --- /dev/null +++ b/javascript/ql/src/semmle/javascript/RangeAnalysis.qll @@ -0,0 +1,366 @@ +import javascript + +/* + * The range analysis is based on Difference Bound constraints, that is, inequalities of form: + * + * a - b <= c + * + * or equivalently, + * + * a <= b + c + * + * where a and b are variables in the constraint system, and c is an integer constant. + * + * Such constraints obey a transitive law. Given two constraints, + * + * a - x <= c1 + * x - b <= c2 + * + * adding the two inequalities yields the obvious transitive conclusion: + * + * a - b <= c1 + c2 + * + * We view the system of constraints as a weighted graph, where `a - b <= c` + * corresponds to the edge `a -> b` with weight `c`. + * + * Paths in this graph corresponds to the additional inequalities we can derive from the constraint set. + * A negative-weight cycle represents a contradiction, such as `a <= a - 1`. + * + * + * CONTROL FLOW: + * + * Each constraint is associated with a CFG node where that constraint is known to be valid. + * The constraint is only valid within the dominator subtree of that node. + * + * The transitive rule additionally requires that, in order to compose two edges, one of + * their CFG nodes must dominate the other, and the resulting edge becomes associated with the + * dominated CFG node (i.e. the most restrictive scope). This ensures constraints + * cannot be taken out of context. + * + * If a negative-weight cycle can be constructed from the edges "in scope" at a given CFG node + * (i.e. associated with a dominator of the node), that node is unreachable. + * + * + * DUAL CONSTRAINTS: + * + * For every data flow node `a` we have two constraint variables, `+a` and `-a` (or just `a` and `-a`) + * representing the numerical value of `a` and its negation. Negations let us reason about the sum of + * two variables. For example: + * + * a + b <= 10 becomes a - (-b) <= 10 + * + * It also lets us reason about the upper and lower bounds of a single variable: + * + * a <= 10 becomes a + a <= 20 becomes a - (-a) <= 20 + * a >= 10 becomes -a <= -10 becomes (-a) - a <= -20 + * + * For the graph analogy to include the relationship between `a` and `-a`, all constraints + * imply their dual constraint: + * + * a - b <= c implies (-b) - (-a) <= c + * + * That is, for every edge from a -> b, there is an edge with the same weight from (-b) -> (-a). + * + * + * PATH FINDING: + * + * See `extendedEdge` predicate for details about how we find negative-weight paths in the graph. + * + * + * CAVEATS: + * + * - We assume !(x <= y) means x > y, ignoring NaN. + * + * - We assume x < y means x <= y + 1, ignoring floats. + * + * - We assume integer arithmetic is exact, ignoring values above 2^53. + * + */ + +/** + * Contains predicates for reasoning about the relative numeric value of expressions. + */ +module RangeAnalysis { + /** + * Holds if the given node has a unique data flow predecessor. + */ + pragma[noinline] + private predicate hasUniquePredecessor(DataFlow::Node node) { + strictcount(node.getAPredecessor()) = 1 + } + + /** + * Gets the definition of `node`, without unfolding phi nodes. + */ + DataFlow::Node getDefinition(DataFlow::Node node) { + if hasUniquePredecessor(node) then + result = getDefinition(node.getAPredecessor()) + else + result = node + } + + /** + * Holds if `r` can be modelled as `r = root * sign + bias`. + * + * Does not follow data flow edges and is not recursive (that is, `root` may itself be defined linearly). + */ + private predicate linearDefinitionStep(DataFlow::Node r, DataFlow::Node root, int sign, int bias) { + not exists(r.asExpr().getIntValue()) and + ( + exists (AddExpr expr | r.asExpr() = expr | + root = expr.getLeftOperand().flow() and + bias = expr.getRightOperand().getIntValue() and + sign = 1) + or + exists (AddExpr expr | r.asExpr() = expr | + bias = expr.getLeftOperand().getIntValue() and + root = expr.getRightOperand().flow() and + sign = 1) + or + exists (SubExpr expr | r.asExpr() = expr | + root = expr.getLeftOperand().flow() and + bias = -expr.getRightOperand().getIntValue() and + sign = 1) + or + exists (SubExpr expr | r.asExpr() = expr | + bias = expr.getLeftOperand().getIntValue() and + root = expr.getRightOperand().flow() and + sign = -1) + or + exists (NegExpr expr | r.asExpr() = expr | + root = expr.getOperand().flow() and + bias = 0 and + sign = -1) + ) + } + + /** + * Holds if `r` can be modelled as `r = root * sign + bias`. + */ + predicate linearDefinition(DataFlow::Node r, DataFlow::Node root, int sign, int bias) { + if hasUniquePredecessor(r) then + linearDefinition(r.getAPredecessor(), root, sign, bias) + else if linearDefinitionStep(r, _, _, _) then + exists (DataFlow::Node pred, int sign1, int bias1, int sign2, int bias2 | + linearDefinitionStep(r, pred, sign1, bias1) and + linearDefinition(pred, root, sign2, bias2) and + sign = sign1 * sign2 and + bias = bias1 + sign1 * bias2) + else ( + root = r and + sign = 1 and + bias = 0 + ) + } + + /** + * Holds if `r` can be modelled as `r = xroot * xsign + yroot * ysign + bias`. + */ + predicate linearDefinitionSum(DataFlow::Node r, DataFlow::Node xroot, int xsign, DataFlow::Node yroot, int ysign, int bias) { + if hasUniquePredecessor(r) then + linearDefinitionSum(r.getAPredecessor(), xroot, xsign, yroot, ysign, bias) + else if exists(r.asExpr().getIntValue()) then + none() // do not model constants as sums + else ( + exists (AddExpr add, int bias1, int bias2 | r.asExpr() = add | + linearDefinition(add.getLeftOperand().flow(), xroot, xsign, bias1) and + linearDefinition(add.getRightOperand().flow(), yroot, ysign, bias2) and + bias = bias1 + bias2) + or + exists (SubExpr sub, int bias1, int bias2 | r.asExpr() = sub | + linearDefinition(sub.getLeftOperand().flow(), xroot, xsign, bias1) and + linearDefinition(sub.getRightOperand().flow(), yroot, -ysign, -bias2) and // Negate right-hand operand + bias = bias1 + bias2) + or + linearDefinitionSum(r.asExpr().(NegExpr).getOperand().flow(), xroot, -xsign, yroot, -ysign, -bias) + ) + } + + /** + * Holds if the given comparison can be modelled as `A B + bias` where `` is the comparison operator. + */ + predicate linearComparison(Comparison comparison, DataFlow::Node a, int asign, DataFlow::Node b, int bsign, int bias) { + exists(Expr left, Expr right, int bias1, int bias2 | left = comparison.getLeftOperand() and right = comparison.getRightOperand() | + // A <= B + c + linearDefinition(left.flow(), a, asign, bias1) and + linearDefinition(right.flow(), b, bsign, bias2) and + bias = bias2 - bias1 + or + // A - B + c1 <= c2 becomes A <= B + (c2 - c1) + linearDefinitionSum(left.flow(), a, asign, b, -bsign, bias1) and + right.getIntValue() = bias2 and + bias = bias2 - bias1 + or + // c1 <= -A + B + c2 becomes A <= B + (c2 - c1) + left.getIntValue() = bias1 and + linearDefinitionSum(right.flow(), a, -asign, b, bsign, bias2) and + bias = bias2 - bias1 + ) + } + + /** + * Holds if `guard` asserts that the outcome of `A B + bias` is true, where `` is a comparison operator. + */ + predicate linearComparisonGuard(ConditionGuardNode guard, DataFlow::Node a, int asign, string operator, DataFlow::Node b, int bsign, int bias) { + exists (Comparison compare | compare = getDefinition(guard.getTest().flow()).asExpr() | + linearComparison(compare, a, asign, b, bsign, bias) and + ( + guard.getOutcome() = true and operator = compare.getOperator() + or + guard.getOutcome() = false and operator = negateOperator(compare.getOperator()) + ) + ) + } + + /** + * Gets the binary operator whose return value is the opposite of `operator` (excluding NaN comparisons). + */ + private string negateOperator(string operator) { + operator = "==" and result = "!=" or + operator = "!=" and result = "==" or + operator = "===" and result = "!==" or + operator = "!==" and result = "===" or + operator = "<" and result = ">=" or + operator = "<=" and result = ">" or + operator = ">" and result = "<=" or + operator = ">=" and result = "<" + } + + /** + * Holds if immediately after `cfg` it becomes known that `A <= B + c`. + * + * These are the initial inputs to the difference bound constraint system. + * + * The dual constraint `-B <= -A + c` is not included in this predicate. + */ + predicate comparisonEdge(ControlFlowNode cfg, DataFlow::Node a, int asign, DataFlow::Node b, int bsign, int bias) { + // A <= B + c + linearComparisonGuard(cfg, a, asign, "<=", b, bsign, bias) + or + // A <= B + c iff A < B + c + 1 (assuming A,B are integers) + linearComparisonGuard(cfg, a, asign, "<", b, bsign, bias + 1) + or + // A <= B + c iff B >= A - c + linearComparisonGuard(cfg, b, bsign, ">=", a, asign, -bias) + or + // A <= B + c iff B > A - c - 1 (assuming A,B are integers) + linearComparisonGuard(cfg, b, bsign, ">", a, asign, -bias - 1) + or + exists (string operator | operator = "==" or operator = "===" | + // A == B + c iff A <= B + c and B <= A - c + linearComparisonGuard(cfg, a, asign, operator, b, bsign, bias) + or + linearComparisonGuard(cfg, b, bsign, operator, a, asign, -bias) + ) + } + + /** + * The set of initial edges including those from dual constraints. + */ + private predicate seedEdge(ControlFlowNode cfg, DataFlow::Node a, int asign, DataFlow::Node b, int bsign, int c) { + // A <= B + c + comparisonEdge(cfg, a, asign, b, bsign, c) + or + // -B <= -A + c (dual constraint) + comparisonEdge(cfg, b, -bsign, a, -asign, c) + } + + /** + * Applies a restricted transitive rule to the edge set. + * + * In particular, we apply the transitive rule only where a negative edge followed by a non-negative edge. + * For example: + * + * A --(-1)--> B --(+3)--> C + * + * yields: + * + * A --(+2)--> C + * + * In practice, the restriction to edges of different sign prevent the + * quadratic blow-up you would normally get from a transitive closure. + * + * It also prevents the relation from becoming infinite in case + * there are negative-weight cycles, where the transitive weights would + * otherwise diverge towards minus infinity. + * + * Moreover, the rule is enough to guarantee the following property: + * + * A negative-weight path from X to Y exists iff a path of negative-weight edges exists from X to Y. + * + * This means negative-weight cycles (contradictions) can be detected using simple cycle detection. + */ + private predicate extendedEdge(ControlFlowNode cfg, DataFlow::Node a, int asign, DataFlow::Node b, int bsign, int c) { + seedEdge(cfg, a, asign, b, bsign, c) + or + exists (DataFlow::Node mid, int midx, ControlFlowNode cfg1, int c1, ControlFlowNode cfg2, int c2 | + extendedEdge(cfg1, a, asign, mid, midx, c1) and + extendedEdge(cfg2, mid, midx, b, bsign, c2) and + c1 < 0 and + c2 >= 0 and + c = c1 + c2 and + // One of the two CFG nodes must dominate the other, and `cfg` must be bound to the dominated one. + ( + // They are in the same basic block + exists (BasicBlock bb, int i, int j | + bb.getNode(i) = cfg1 and + bb.getNode(j) = cfg2 and + if i < j then + cfg = cfg2 + else + cfg = cfg1) + or + // They are in different basic blocks + cfg1.getBasicBlock().(ReachableBasicBlock).strictlyDominates(cfg2.getBasicBlock()) and + cfg = cfg2 + or + cfg2.getBasicBlock().(ReachableBasicBlock).strictlyDominates(cfg1.getBasicBlock()) and + cfg = cfg1 + ) + ) + } + + /** + * Holds if there is a negative-weight edge from src to dst. + */ + private predicate negativeEdge(ControlFlowNode cfg, DataFlow::Node a, int asign, DataFlow::Node b, int bsign) { + exists (int weight | extendedEdge(cfg, a, asign, b, bsign, weight) | + weight < 0) + } + + /** + * Holds if `src` can reach `dst` using only negative-weight edges. + * + * The initial outgoing edge from `src` must be derived at `cfg`. + */ + pragma[noopt] + private predicate reachableByNegativeEdges(ControlFlowNode cfg, DataFlow::Node a, int asign, DataFlow::Node b, int bsign) { + negativeEdge(cfg, a, asign, b, bsign) + or + exists(DataFlow::Node mid, int midx, ControlFlowNode midcfg | + reachableByNegativeEdges(cfg, a, asign, mid, midx) and + negativeEdge(midcfg, mid, midx, b, bsign) and + exists (BasicBlock bb, int i, int j | + bb.getNode(i) = midcfg and + bb.getNode(j) = cfg and + i <= j)) + or + // Same as above, but where CFG nodes are in different basic blocks + exists(DataFlow::Node mid, int midx, ControlFlowNode midcfg, BasicBlock midBB, ReachableBasicBlock midRBB, BasicBlock cfgBB | + reachableByNegativeEdges(cfg, a, asign, mid, midx) and + negativeEdge(midcfg, mid, midx, b, bsign) and + midBB = midcfg.getBasicBlock() and + midRBB = midBB.(ReachableBasicBlock) and + cfgBB = cfg.getBasicBlock() and + midRBB.strictlyDominates(cfgBB) + ) + } + + /** + * Holds if the condition asserted at `guard` is contradictory, that is, its condition always has the + * opposite of the expected outcome. + */ + predicate isContradictoryGuardNode(ConditionGuardNode guard) { + exists (DataFlow::Node a, int asign | reachableByNegativeEdges(guard, a, asign, a, asign)) + } +} diff --git a/javascript/ql/test/library-tests/RangeAnalysis/DeadBranch.expected b/javascript/ql/test/library-tests/RangeAnalysis/DeadBranch.expected new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/javascript/ql/test/library-tests/RangeAnalysis/DeadBranch.ql b/javascript/ql/test/library-tests/RangeAnalysis/DeadBranch.ql new file mode 100644 index 000000000000..838e45c77e47 --- /dev/null +++ b/javascript/ql/test/library-tests/RangeAnalysis/DeadBranch.ql @@ -0,0 +1,35 @@ +import javascript + +class AssertionComment extends LineComment { + boolean isOK; + + AssertionComment() { + isOK = true and getText().trim().regexpMatch("OK.*") + or + isOK = false and getText().trim().regexpMatch("NOT OK.*") + } + + ConditionGuardNode getAGuardNode() { + result.getLocation().getStartLine() = this.getLocation().getStartLine() and + result.getFile() = this.getFile() + } + + Expr getTestExpr() { result = getAGuardNode().getTest() } + + string getMessage() { + not exists(getAGuardNode()) and result = "Error: no guard node on this line" + or + isOK = true and + exists (ConditionGuardNode guard | guard = getAGuardNode() | + RangeAnalysis::isContradictoryGuardNode(guard) and + result = "Error: analysis claims " + getTestExpr() + " is always " + guard.getOutcome().booleanNot() + ) + or + isOK = false and + not RangeAnalysis::isContradictoryGuardNode(getAGuardNode()) and + result = "Error: " + getTestExpr() + " is always true or always false" + } +} + +from AssertionComment assertion +select assertion, assertion.getMessage() diff --git a/javascript/ql/test/library-tests/RangeAnalysis/plus.js b/javascript/ql/test/library-tests/RangeAnalysis/plus.js new file mode 100644 index 000000000000..ac430bcba4cd --- /dev/null +++ b/javascript/ql/test/library-tests/RangeAnalysis/plus.js @@ -0,0 +1,28 @@ +function f(x,y) { + if (x + 1 < y) { + if (x < y) {} // NOT OK - always true + if (x + 1 < y) {} // NOT OK - always true + if (x + 1 <= y) {} // NOT OK - always true + if (x > y) {} // NOT OK - always false + if (x >= y) {} // NOT OK - always false + if (x + 1 >= y) {} // NOT OK - always false + if (x + 2 < y) {} // OK + if (x < y - 1) {} // NOT OK - always true + } + if (x < y + 1) { + if (x < y) {} // OK + if (x + 1 < y) {} // OK + if (x - 1 < y) {} // NOT OK - always true + } +} + +function g(x) { + let z = x + 1; + z += 1; + ++z; + z++; + if (z < x + 4) {} // NOT OK - always false + if (z > x + 4) {} // NOT OK - always false + if (z < x + 5) {} // NOT OK - always true + if (z > x + 5) {} // NOT OK - always false +} diff --git a/javascript/ql/test/library-tests/RangeAnalysis/tst.js b/javascript/ql/test/library-tests/RangeAnalysis/tst.js new file mode 100644 index 000000000000..f89086711f6b --- /dev/null +++ b/javascript/ql/test/library-tests/RangeAnalysis/tst.js @@ -0,0 +1,52 @@ +function f(x,y) { + if (x < y) { + if (x < y) {} // NOT OK - always true + if (x <= y) {} // NOT OK - always true + if (x > y) {} // NOT OK - always false + if (x >= y) {} // NOT OK - always false + if (x === y) {} // NOT OK - always false + } else { + if (x < y) {} // NOT OK - always false + if (x <= y) {} // OK - could be equal + if (x > y) {} // OK + if (x >= y) {} // NOT OK - always true + if (x === y) {} // OK + } +} + +function g(x,y) { + if (x <= y) { + if (x < y) {} // OK + if (x <= y) {} // NOT OK - always true + if (x > y) {} // NOT OK - always false + if (x >= y) {} // OK - could be equal + if (x === y) {} // OK + } else { + if (x < y) {} // NOT OK - always false + if (x <= y) {} // NOT OK - always false + if (x > y) {} // NOT OK - always true + if (x >= y) {} // NOT OK - always true + if (x === y) {} // NOT OK - always false + } +} + +function loop(start, end) { + var i; + for (i = start; i < end; i++) { + if (i < end) {} // NOT OK - always true + if (i >= end) {} // NOT OK - always false + } + if (i >= end) {} // NOT OK - always true + if (i < end) {} // NOT OK - always false +} + +function h(x, y) { + if (x - y < 0) { + if (x < y) {} // NOT OK - always true + if (x >= y) {} // NOT OK - always false + } + if (0 < x - y) { // y < x + if (x <= y) {} // NOT OK - always false + if (x > y) {} // NOT OK - always true + } +} From 73cbdee691d895a9949b0f2949bf4e7fe11ac96c Mon Sep 17 00:00:00 2001 From: Asger F Date: Wed, 19 Sep 2018 18:28:18 +0100 Subject: [PATCH 02/27] JS: Compound assignments and update exprs in range analysis --- .../src/semmle/javascript/RangeAnalysis.qll | 44 ++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/javascript/ql/src/semmle/javascript/RangeAnalysis.qll b/javascript/ql/src/semmle/javascript/RangeAnalysis.qll index c4c8a287bc5a..9e1cb9b76623 100644 --- a/javascript/ql/src/semmle/javascript/RangeAnalysis.qll +++ b/javascript/ql/src/semmle/javascript/RangeAnalysis.qll @@ -99,10 +99,33 @@ module RangeAnalysis { result = node } + /** + * Gets a data flow node holding the result of the add/subtract operation in + * the given increment/decrement expression. + */ + private DataFlow::Node updateExprResult(UpdateExpr expr) { + exists (SsaExplicitDefinition def | def.getDef() = expr | + result = DataFlow::ssaDefinitionNode(def)) + or + expr.isPrefix() and + result = expr.flow() + } + + /** + * Gets a data flow node holding the result of the given componund assignment. + */ + private DataFlow::Node compoundAssignResult(CompoundAssignExpr expr) { + exists (SsaExplicitDefinition def | def.getDef() = expr | + result = DataFlow::ssaDefinitionNode(def)) + or + result = expr.flow() + } + /** * Holds if `r` can be modelled as `r = root * sign + bias`. * - * Does not follow data flow edges and is not recursive (that is, `root` may itself be defined linearly). + * Only looks "one step", that is, does not follow data flow and does not recursively + * unfold nested arithmetic expressions. */ private predicate linearDefinitionStep(DataFlow::Node r, DataFlow::Node root, int sign, int bias) { not exists(r.asExpr().getIntValue()) and @@ -131,6 +154,25 @@ module RangeAnalysis { root = expr.getOperand().flow() and bias = 0 and sign = -1) + or + exists (UpdateExpr update | r = updateExprResult(update) | + root = update.getOperand().flow() and + sign = 1 and + if update instanceof IncExpr then + bias = 1 + else + bias = -1) + or + exists (CompoundAssignExpr assign | r = compoundAssignResult(assign) | + root = assign.getLhs().flow() and + sign = 1 and + ( + assign instanceof AssignAddExpr and + bias = assign.getRhs().getIntValue() + or + assign instanceof AssignSubExpr and + bias = -assign.getRhs().getIntValue() + )) ) } From 09ca6652fb0397289ebe168212d149fcc4c91606 Mon Sep 17 00:00:00 2001 From: Asger F Date: Wed, 19 Sep 2018 18:45:55 +0100 Subject: [PATCH 03/27] JS: Support return value of x++ --- javascript/ql/src/semmle/javascript/RangeAnalysis.qll | 6 ++++++ javascript/ql/test/library-tests/RangeAnalysis/plus.js | 5 +++++ 2 files changed, 11 insertions(+) diff --git a/javascript/ql/src/semmle/javascript/RangeAnalysis.qll b/javascript/ql/src/semmle/javascript/RangeAnalysis.qll index 9e1cb9b76623..1eafe1215a9f 100644 --- a/javascript/ql/src/semmle/javascript/RangeAnalysis.qll +++ b/javascript/ql/src/semmle/javascript/RangeAnalysis.qll @@ -163,6 +163,12 @@ module RangeAnalysis { else bias = -1) or + exists (UpdateExpr update | r.asExpr() = update | // Return value of x++ is just x (coerced to an int) + root = update.getOperand().flow() and + not update.isPrefix() and + sign = 1 and + bias = 0) + or exists (CompoundAssignExpr assign | r = compoundAssignResult(assign) | root = assign.getLhs().flow() and sign = 1 and diff --git a/javascript/ql/test/library-tests/RangeAnalysis/plus.js b/javascript/ql/test/library-tests/RangeAnalysis/plus.js index ac430bcba4cd..3b997a49371f 100644 --- a/javascript/ql/test/library-tests/RangeAnalysis/plus.js +++ b/javascript/ql/test/library-tests/RangeAnalysis/plus.js @@ -26,3 +26,8 @@ function g(x) { if (z < x + 5) {} // NOT OK - always true if (z > x + 5) {} // NOT OK - always false } + +function h(x) { + let y = x++; + if (x > y) {} // NOT OK - always true +} From 064b1099ebf3b6468caa9e59bee54d4077c8979f Mon Sep 17 00:00:00 2001 From: Asger F Date: Thu, 20 Sep 2018 16:47:57 +0100 Subject: [PATCH 04/27] JS: range analysis through phi nodes --- .../src/semmle/javascript/RangeAnalysis.qll | 76 ++++++++++++++++++- .../test/library-tests/RangeAnalysis/loop.js | 40 ++++++++++ 2 files changed, 114 insertions(+), 2 deletions(-) create mode 100644 javascript/ql/test/library-tests/RangeAnalysis/loop.js diff --git a/javascript/ql/src/semmle/javascript/RangeAnalysis.qll b/javascript/ql/src/semmle/javascript/RangeAnalysis.qll index 1eafe1215a9f..c91d2ad0e5aa 100644 --- a/javascript/ql/src/semmle/javascript/RangeAnalysis.qll +++ b/javascript/ql/src/semmle/javascript/RangeAnalysis.qll @@ -302,6 +302,71 @@ module RangeAnalysis { ) } + /** + * Holds if `node` is a phi node with `left` and `right` has the only two inputs. + * + * Note that this predicate is symmetric: when it holds for (left, right) it also holds for (right, left). + */ + predicate binaryPhiNode(DataFlow::Node node, DataFlow::Node left, DataFlow::Node right) { + exists (SsaPhiNode phi | node = DataFlow::ssaDefinitionNode(phi) | + strictcount(phi.getAnInput()) = 2 and + left = DataFlow::ssaDefinitionNode(phi.getAnInput()) and + right = DataFlow::ssaDefinitionNode(phi.getAnInput()) and + left != right) + } + + /** + * Holds if `A <= B + c` can be determined based on a phi node. + */ + predicate phiEdge(ControlFlowNode cfg, DataFlow::Node a, int asign, DataFlow::Node b, int bsign, int c) { + exists (DataFlow::Node phi, DataFlow::Node left, DataFlow::Node right | + binaryPhiNode(phi, left, right) and + cfg = phi.getBasicBlock() + | + // Both inputs are defined in terms of the same root: + // phi = PHI(root + bias1, root + bias2) + exists (DataFlow::Node root, int sign, int bias1, int bias2 | + linearDefinition(left, root, sign, bias1) and + linearDefinition(right, root, sign, bias2) and + bias1 < bias2 and + // root + bias1 <= phi <= root + bias2 + ( + // root <= phi - bias1 + a = root and asign = 1 and + b = phi and bsign = 1 and + c = -bias1 + or + // phi <= root + bias2 + a = phi and asign = 1 and + b = root and bsign = 1 and + c = bias2 + ) + ) + or + // One input is defined in terms of the phi node itself: + // phi = PHI(phi + increment, x) + exists (int increment, DataFlow::Node root, int sign, int bias | + linearDefinition(left, phi, 1, increment) and + linearDefinition(right, root, sign, bias) and + ( + // If increment is positive (or zero): + // phi >= right' + bias + increment >= 0 and + a = root and asign = sign and + b = phi and bsign = 1 and + c = -bias + or + // If increment is negative (or zero): + // phi <= right' + bias + increment <= 0 and + a = phi and asign = 1 and + b = root and bsign = sign and + c = bias + ) + ) + ) + } + /** * The set of initial edges including those from dual constraints. */ @@ -309,8 +374,15 @@ module RangeAnalysis { // A <= B + c comparisonEdge(cfg, a, asign, b, bsign, c) or + phiEdge(cfg, a, asign, b, bsign, c) + } + + private predicate seedEdgeWithDual(ControlFlowNode cfg, DataFlow::Node a, int asign, DataFlow::Node b, int bsign, int c) { + // A <= B + c + seedEdge(cfg, a, asign, b, bsign, c) + or // -B <= -A + c (dual constraint) - comparisonEdge(cfg, b, -bsign, a, -asign, c) + seedEdge(cfg, b, -bsign, a, -asign, c) } /** @@ -339,7 +411,7 @@ module RangeAnalysis { * This means negative-weight cycles (contradictions) can be detected using simple cycle detection. */ private predicate extendedEdge(ControlFlowNode cfg, DataFlow::Node a, int asign, DataFlow::Node b, int bsign, int c) { - seedEdge(cfg, a, asign, b, bsign, c) + seedEdgeWithDual(cfg, a, asign, b, bsign, c) or exists (DataFlow::Node mid, int midx, ControlFlowNode cfg1, int c1, ControlFlowNode cfg2, int c2 | extendedEdge(cfg1, a, asign, mid, midx, c1) and diff --git a/javascript/ql/test/library-tests/RangeAnalysis/loop.js b/javascript/ql/test/library-tests/RangeAnalysis/loop.js new file mode 100644 index 000000000000..57bbe2a0262b --- /dev/null +++ b/javascript/ql/test/library-tests/RangeAnalysis/loop.js @@ -0,0 +1,40 @@ +function increasing(start, end) { + for (var i = start; i < end; ++i) { + if (i >= start) {} // NOT OK - always true + if (i < start) {} // NOT OK - always false + + if (i < end) {} // NOT OK - always true + if (i >= end) {} // NOT OK - always false + + if (i + 1 > start) {} // NOT OK - always true + if (i - 1 < start) {} // OK + } +} + +function decreasing(start, end) { + for (var i = start; i > end; --i) { + if (i <= start) {} // NOT OK - always true + if (i > start) {} // NOT OK - always false + + if (i + 1 > start) {} // OK + if (i - 1 < start) {} // NOT OK - always true + } +} + +function middleIncrement(start, end) { + for (var i = start; i < end;) { + if (i >= start) {} // OK - always true but we don't catch it + if (i < start) {} // OK - always false but we don't catch it + + if (i < end) {} // NOT OK - always true + if (i >= end) {} // NOT OK - always false + + if (something()) { + i += 2; + } + if (i >= start) {} // OK - always true but we don't catch it + if (i < start) {} // OK - always false but we don't catch it + if (i < end) {} // OK + if (i >= end) {} // OK + } +} From 6c53ad80c746b4f4940af5cc124d154c8718a20e Mon Sep 17 00:00:00 2001 From: Asger F Date: Thu, 20 Sep 2018 17:24:13 +0100 Subject: [PATCH 05/27] JS: add constant constraints in range analysis --- .../src/semmle/javascript/RangeAnalysis.qll | 19 +++++++++++++++++++ .../library-tests/RangeAnalysis/constants.js | 8 ++++++++ 2 files changed, 27 insertions(+) create mode 100644 javascript/ql/test/library-tests/RangeAnalysis/constants.js diff --git a/javascript/ql/src/semmle/javascript/RangeAnalysis.qll b/javascript/ql/src/semmle/javascript/RangeAnalysis.qll index c91d2ad0e5aa..6451c62a0cf2 100644 --- a/javascript/ql/src/semmle/javascript/RangeAnalysis.qll +++ b/javascript/ql/src/semmle/javascript/RangeAnalysis.qll @@ -367,6 +367,23 @@ module RangeAnalysis { ) } + /** + * Holds constraints derived from `A = const`. + * + * `A = c` is written to `A + A = 2c` which is then converted to `<=` and `>=`. + * + * A + A <= 2c becomes A <= -A + 2c + * A + A >= 2c becomes -A <= A - 2c + */ + predicate constantEdge(ControlFlowNode cfg, DataFlow::Node a, int asign, DataFlow::Node b, int bsign, int c) { + exists (NumberLiteral literal | cfg = literal | + a = literal.flow() and + b = a and + (asign = 1 or asign = -1) and + bsign = -asign and + c = literal.getIntValue() * 2 * asign) + } + /** * The set of initial edges including those from dual constraints. */ @@ -375,6 +392,8 @@ module RangeAnalysis { comparisonEdge(cfg, a, asign, b, bsign, c) or phiEdge(cfg, a, asign, b, bsign, c) + or + constantEdge(cfg, a, asign, b, bsign, c) } private predicate seedEdgeWithDual(ControlFlowNode cfg, DataFlow::Node a, int asign, DataFlow::Node b, int bsign, int c) { diff --git a/javascript/ql/test/library-tests/RangeAnalysis/constants.js b/javascript/ql/test/library-tests/RangeAnalysis/constants.js new file mode 100644 index 000000000000..9e1a3b914e3c --- /dev/null +++ b/javascript/ql/test/library-tests/RangeAnalysis/constants.js @@ -0,0 +1,8 @@ +function f() { + if (1 > 0) {} // NOT OK - always true + if (1 - 1 >= 0) {} // NOT OK - always true + let one = 1; + let two = 2; + if (two > one) {} // NOT OK - always true + if (two <= one) {} // NOT OK - always false +} From 9d8d953292b6d93bacd890a649374ad0571db80e Mon Sep 17 00:00:00 2001 From: Asger F Date: Tue, 25 Sep 2018 16:50:29 +0100 Subject: [PATCH 06/27] JS: perform widening when adding operands of very different magnitude --- .../src/semmle/javascript/RangeAnalysis.qll | 24 ++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/javascript/ql/src/semmle/javascript/RangeAnalysis.qll b/javascript/ql/src/semmle/javascript/RangeAnalysis.qll index 6451c62a0cf2..5669dad514ae 100644 --- a/javascript/ql/src/semmle/javascript/RangeAnalysis.qll +++ b/javascript/ql/src/semmle/javascript/RangeAnalysis.qll @@ -404,6 +404,26 @@ module RangeAnalysis { seedEdge(cfg, b, -bsign, a, -asign, c) } + /** + * Adds a negative and positive integer, but only if they are within in the same + * order of magnitude. + */ + bindingset[x, y] + private int wideningAddition(int x, int y) { + x < 0 and + y >= 0 and + ( + x = 0 + or + y = 0 + or + // If non-zero, check that the values are within a factor 16 of each other + x.abs().bitShiftRight(4) < y.abs() and + y.abs().bitShiftRight(4) < x.abs() + ) and + result = x + y + } + /** * Applies a restricted transitive rule to the edge set. * @@ -435,9 +455,7 @@ module RangeAnalysis { exists (DataFlow::Node mid, int midx, ControlFlowNode cfg1, int c1, ControlFlowNode cfg2, int c2 | extendedEdge(cfg1, a, asign, mid, midx, c1) and extendedEdge(cfg2, mid, midx, b, bsign, c2) and - c1 < 0 and - c2 >= 0 and - c = c1 + c2 and + c = wideningAddition(c1, c2) and // One of the two CFG nodes must dominate the other, and `cfg` must be bound to the dominated one. ( // They are in the same basic block From 20aa4e1f6dc434acaf84a81fda043993e7186e72 Mon Sep 17 00:00:00 2001 From: Asger F Date: Fri, 28 Sep 2018 18:20:13 +0100 Subject: [PATCH 07/27] JS: handle sharp inequalities directly --- .../src/semmle/javascript/RangeAnalysis.qll | 60 ++++++++++--------- 1 file changed, 33 insertions(+), 27 deletions(-) diff --git a/javascript/ql/src/semmle/javascript/RangeAnalysis.qll b/javascript/ql/src/semmle/javascript/RangeAnalysis.qll index 5669dad514ae..f8ccfa441593 100644 --- a/javascript/ql/src/semmle/javascript/RangeAnalysis.qll +++ b/javascript/ql/src/semmle/javascript/RangeAnalysis.qll @@ -71,8 +71,6 @@ import javascript * * - We assume !(x <= y) means x > y, ignoring NaN. * - * - We assume x < y means x <= y + 1, ignoring floats. - * * - We assume integer arithmetic is exact, ignoring values above 2^53. * */ @@ -281,19 +279,24 @@ module RangeAnalysis { * * The dual constraint `-B <= -A + c` is not included in this predicate. */ - predicate comparisonEdge(ControlFlowNode cfg, DataFlow::Node a, int asign, DataFlow::Node b, int bsign, int bias) { + predicate comparisonEdge(ControlFlowNode cfg, DataFlow::Node a, int asign, DataFlow::Node b, int bsign, int bias, boolean sharp) { // A <= B + c - linearComparisonGuard(cfg, a, asign, "<=", b, bsign, bias) + linearComparisonGuard(cfg, a, asign, "<=", b, bsign, bias) and + sharp = false or - // A <= B + c iff A < B + c + 1 (assuming A,B are integers) - linearComparisonGuard(cfg, a, asign, "<", b, bsign, bias + 1) + // A < B + c + linearComparisonGuard(cfg, a, asign, "<", b, bsign, bias) and + sharp = true or // A <= B + c iff B >= A - c - linearComparisonGuard(cfg, b, bsign, ">=", a, asign, -bias) + linearComparisonGuard(cfg, b, bsign, ">=", a, asign, -bias) and + sharp = false or - // A <= B + c iff B > A - c - 1 (assuming A,B are integers) - linearComparisonGuard(cfg, b, bsign, ">", a, asign, -bias - 1) + // A < B + c iff B > A - c + linearComparisonGuard(cfg, b, bsign, ">", a, asign, -bias) and + sharp = true or + sharp = false and exists (string operator | operator = "==" or operator = "===" | // A == B + c iff A <= B + c and B <= A - c linearComparisonGuard(cfg, a, asign, operator, b, bsign, bias) @@ -387,31 +390,31 @@ module RangeAnalysis { /** * The set of initial edges including those from dual constraints. */ - private predicate seedEdge(ControlFlowNode cfg, DataFlow::Node a, int asign, DataFlow::Node b, int bsign, int c) { + private predicate seedEdge(ControlFlowNode cfg, DataFlow::Node a, int asign, DataFlow::Node b, int bsign, int c, boolean sharp) { // A <= B + c - comparisonEdge(cfg, a, asign, b, bsign, c) + comparisonEdge(cfg, a, asign, b, bsign, c, sharp) or - phiEdge(cfg, a, asign, b, bsign, c) + phiEdge(cfg, a, asign, b, bsign, c) and sharp = false or - constantEdge(cfg, a, asign, b, bsign, c) + constantEdge(cfg, a, asign, b, bsign, c) and sharp = false } - private predicate seedEdgeWithDual(ControlFlowNode cfg, DataFlow::Node a, int asign, DataFlow::Node b, int bsign, int c) { + private predicate seedEdgeWithDual(ControlFlowNode cfg, DataFlow::Node a, int asign, DataFlow::Node b, int bsign, int c, boolean sharp) { // A <= B + c - seedEdge(cfg, a, asign, b, bsign, c) + seedEdge(cfg, a, asign, b, bsign, c, sharp) or // -B <= -A + c (dual constraint) - seedEdge(cfg, b, -bsign, a, -asign, c) + seedEdge(cfg, b, -bsign, a, -asign, c, sharp) } /** * Adds a negative and positive integer, but only if they are within in the same * order of magnitude. */ - bindingset[x, y] - private int wideningAddition(int x, int y) { - x < 0 and - y >= 0 and + bindingset[x, sharpx, y, sharpy] + private int wideningAddition(int x, boolean sharpx, int y, boolean sharpy) { + (x < 0 or x = 0 and sharpx = true) and + (y > 0 or y = 0 and sharpy = false) and ( x = 0 or @@ -449,13 +452,14 @@ module RangeAnalysis { * * This means negative-weight cycles (contradictions) can be detected using simple cycle detection. */ - private predicate extendedEdge(ControlFlowNode cfg, DataFlow::Node a, int asign, DataFlow::Node b, int bsign, int c) { - seedEdgeWithDual(cfg, a, asign, b, bsign, c) + private predicate extendedEdge(ControlFlowNode cfg, DataFlow::Node a, int asign, DataFlow::Node b, int bsign, int c, boolean sharp) { + seedEdgeWithDual(cfg, a, asign, b, bsign, c, sharp) or - exists (DataFlow::Node mid, int midx, ControlFlowNode cfg1, int c1, ControlFlowNode cfg2, int c2 | - extendedEdge(cfg1, a, asign, mid, midx, c1) and - extendedEdge(cfg2, mid, midx, b, bsign, c2) and - c = wideningAddition(c1, c2) and + exists (DataFlow::Node mid, int midx, ControlFlowNode cfg1, int c1, ControlFlowNode cfg2, int c2, boolean sharp1, boolean sharp2 | + extendedEdge(cfg1, a, asign, mid, midx, c1, sharp1) and + extendedEdge(cfg2, mid, midx, b, bsign, c2, sharp2) and + sharp = sharp1.booleanOr(sharp2) and + c = wideningAddition(c1, sharp1, c2, sharp2) and // One of the two CFG nodes must dominate the other, and `cfg` must be bound to the dominated one. ( // They are in the same basic block @@ -481,7 +485,9 @@ module RangeAnalysis { * Holds if there is a negative-weight edge from src to dst. */ private predicate negativeEdge(ControlFlowNode cfg, DataFlow::Node a, int asign, DataFlow::Node b, int bsign) { - exists (int weight | extendedEdge(cfg, a, asign, b, bsign, weight) | + exists (int weight, boolean sharp | extendedEdge(cfg, a, asign, b, bsign, weight, sharp) | + weight = 0 and sharp = true // a strict "< 0" edge counts as negative + or weight < 0) } From feb8a8c4fd11fad881f1ec3072806a8c84bd21d2 Mon Sep 17 00:00:00 2001 From: Asger F Date: Thu, 4 Oct 2018 11:47:11 +0100 Subject: [PATCH 08/27] JS: restrict bias to 30-bit range to avoid overflow --- .../src/semmle/javascript/RangeAnalysis.qll | 42 ++++++++++++------- 1 file changed, 28 insertions(+), 14 deletions(-) diff --git a/javascript/ql/src/semmle/javascript/RangeAnalysis.qll b/javascript/ql/src/semmle/javascript/RangeAnalysis.qll index f8ccfa441593..2010823d5f39 100644 --- a/javascript/ql/src/semmle/javascript/RangeAnalysis.qll +++ b/javascript/ql/src/semmle/javascript/RangeAnalysis.qll @@ -119,13 +119,26 @@ module RangeAnalysis { result = expr.flow() } + /** + * A 30-bit integer. + * + * Adding two such integers is guaranteed not to overflow. We simply omit constraints + * whose parameters would exceed this range. + */ + private class Bias extends int { + bindingset[this] + Bias() { + -536870912 < this and this < 536870912 + } + } + /** * Holds if `r` can be modelled as `r = root * sign + bias`. * * Only looks "one step", that is, does not follow data flow and does not recursively * unfold nested arithmetic expressions. */ - private predicate linearDefinitionStep(DataFlow::Node r, DataFlow::Node root, int sign, int bias) { + private predicate linearDefinitionStep(DataFlow::Node r, DataFlow::Node root, int sign, Bias bias) { not exists(r.asExpr().getIntValue()) and ( exists (AddExpr expr | r.asExpr() = expr | @@ -183,7 +196,7 @@ module RangeAnalysis { /** * Holds if `r` can be modelled as `r = root * sign + bias`. */ - predicate linearDefinition(DataFlow::Node r, DataFlow::Node root, int sign, int bias) { + predicate linearDefinition(DataFlow::Node r, DataFlow::Node root, int sign, Bias bias) { if hasUniquePredecessor(r) then linearDefinition(r.getAPredecessor(), root, sign, bias) else if linearDefinitionStep(r, _, _, _) then @@ -202,7 +215,7 @@ module RangeAnalysis { /** * Holds if `r` can be modelled as `r = xroot * xsign + yroot * ysign + bias`. */ - predicate linearDefinitionSum(DataFlow::Node r, DataFlow::Node xroot, int xsign, DataFlow::Node yroot, int ysign, int bias) { + predicate linearDefinitionSum(DataFlow::Node r, DataFlow::Node xroot, int xsign, DataFlow::Node yroot, int ysign, Bias bias) { if hasUniquePredecessor(r) then linearDefinitionSum(r.getAPredecessor(), xroot, xsign, yroot, ysign, bias) else if exists(r.asExpr().getIntValue()) then @@ -225,7 +238,7 @@ module RangeAnalysis { /** * Holds if the given comparison can be modelled as `A B + bias` where `` is the comparison operator. */ - predicate linearComparison(Comparison comparison, DataFlow::Node a, int asign, DataFlow::Node b, int bsign, int bias) { + predicate linearComparison(Comparison comparison, DataFlow::Node a, int asign, DataFlow::Node b, int bsign, Bias bias) { exists(Expr left, Expr right, int bias1, int bias2 | left = comparison.getLeftOperand() and right = comparison.getRightOperand() | // A <= B + c linearDefinition(left.flow(), a, asign, bias1) and @@ -247,7 +260,7 @@ module RangeAnalysis { /** * Holds if `guard` asserts that the outcome of `A B + bias` is true, where `` is a comparison operator. */ - predicate linearComparisonGuard(ConditionGuardNode guard, DataFlow::Node a, int asign, string operator, DataFlow::Node b, int bsign, int bias) { + predicate linearComparisonGuard(ConditionGuardNode guard, DataFlow::Node a, int asign, string operator, DataFlow::Node b, int bsign, Bias bias) { exists (Comparison compare | compare = getDefinition(guard.getTest().flow()).asExpr() | linearComparison(compare, a, asign, b, bsign, bias) and ( @@ -279,7 +292,7 @@ module RangeAnalysis { * * The dual constraint `-B <= -A + c` is not included in this predicate. */ - predicate comparisonEdge(ControlFlowNode cfg, DataFlow::Node a, int asign, DataFlow::Node b, int bsign, int bias, boolean sharp) { + predicate comparisonEdge(ControlFlowNode cfg, DataFlow::Node a, int asign, DataFlow::Node b, int bsign, Bias bias, boolean sharp) { // A <= B + c linearComparisonGuard(cfg, a, asign, "<=", b, bsign, bias) and sharp = false @@ -321,14 +334,14 @@ module RangeAnalysis { /** * Holds if `A <= B + c` can be determined based on a phi node. */ - predicate phiEdge(ControlFlowNode cfg, DataFlow::Node a, int asign, DataFlow::Node b, int bsign, int c) { + predicate phiEdge(ControlFlowNode cfg, DataFlow::Node a, int asign, DataFlow::Node b, int bsign, Bias c) { exists (DataFlow::Node phi, DataFlow::Node left, DataFlow::Node right | binaryPhiNode(phi, left, right) and cfg = phi.getBasicBlock() | // Both inputs are defined in terms of the same root: // phi = PHI(root + bias1, root + bias2) - exists (DataFlow::Node root, int sign, int bias1, int bias2 | + exists (DataFlow::Node root, int sign, Bias bias1, Bias bias2 | linearDefinition(left, root, sign, bias1) and linearDefinition(right, root, sign, bias2) and bias1 < bias2 and @@ -348,7 +361,7 @@ module RangeAnalysis { or // One input is defined in terms of the phi node itself: // phi = PHI(phi + increment, x) - exists (int increment, DataFlow::Node root, int sign, int bias | + exists (int increment, DataFlow::Node root, int sign, Bias bias | linearDefinition(left, phi, 1, increment) and linearDefinition(right, root, sign, bias) and ( @@ -378,8 +391,9 @@ module RangeAnalysis { * A + A <= 2c becomes A <= -A + 2c * A + A >= 2c becomes -A <= A - 2c */ - predicate constantEdge(ControlFlowNode cfg, DataFlow::Node a, int asign, DataFlow::Node b, int bsign, int c) { + predicate constantEdge(ControlFlowNode cfg, DataFlow::Node a, int asign, DataFlow::Node b, int bsign, Bias c) { exists (NumberLiteral literal | cfg = literal | + literal.getIntValue() instanceof Bias and // avoid overflow a = literal.flow() and b = a and (asign = 1 or asign = -1) and @@ -390,7 +404,7 @@ module RangeAnalysis { /** * The set of initial edges including those from dual constraints. */ - private predicate seedEdge(ControlFlowNode cfg, DataFlow::Node a, int asign, DataFlow::Node b, int bsign, int c, boolean sharp) { + predicate seedEdge(ControlFlowNode cfg, DataFlow::Node a, int asign, DataFlow::Node b, int bsign, Bias c, boolean sharp) { // A <= B + c comparisonEdge(cfg, a, asign, b, bsign, c, sharp) or @@ -399,7 +413,7 @@ module RangeAnalysis { constantEdge(cfg, a, asign, b, bsign, c) and sharp = false } - private predicate seedEdgeWithDual(ControlFlowNode cfg, DataFlow::Node a, int asign, DataFlow::Node b, int bsign, int c, boolean sharp) { + private predicate seedEdgeWithDual(ControlFlowNode cfg, DataFlow::Node a, int asign, DataFlow::Node b, int bsign, Bias c, boolean sharp) { // A <= B + c seedEdge(cfg, a, asign, b, bsign, c, sharp) or @@ -452,10 +466,10 @@ module RangeAnalysis { * * This means negative-weight cycles (contradictions) can be detected using simple cycle detection. */ - private predicate extendedEdge(ControlFlowNode cfg, DataFlow::Node a, int asign, DataFlow::Node b, int bsign, int c, boolean sharp) { + private predicate extendedEdge(ControlFlowNode cfg, DataFlow::Node a, int asign, DataFlow::Node b, int bsign, Bias c, boolean sharp) { seedEdgeWithDual(cfg, a, asign, b, bsign, c, sharp) or - exists (DataFlow::Node mid, int midx, ControlFlowNode cfg1, int c1, ControlFlowNode cfg2, int c2, boolean sharp1, boolean sharp2 | + exists (DataFlow::Node mid, int midx, ControlFlowNode cfg1, Bias c1, ControlFlowNode cfg2, Bias c2, boolean sharp1, boolean sharp2 | extendedEdge(cfg1, a, asign, mid, midx, c1, sharp1) and extendedEdge(cfg2, mid, midx, b, bsign, c2, sharp2) and sharp = sharp1.booleanOr(sharp2) and From 43df9538bf7d9f676be2aee09b3db0a86dc5d557 Mon Sep 17 00:00:00 2001 From: Asger F Date: Tue, 23 Oct 2018 15:36:41 +0100 Subject: [PATCH 09/27] JS: be conservative in presence of NaN comments --- .../src/semmle/javascript/RangeAnalysis.qll | 21 ++++++++++++++++++- .../test/library-tests/RangeAnalysis/tst.js | 12 +++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/javascript/ql/src/semmle/javascript/RangeAnalysis.qll b/javascript/ql/src/semmle/javascript/RangeAnalysis.qll index 2010823d5f39..e944b82494e9 100644 --- a/javascript/ql/src/semmle/javascript/RangeAnalysis.qll +++ b/javascript/ql/src/semmle/javascript/RangeAnalysis.qll @@ -69,7 +69,7 @@ import javascript * * CAVEATS: * - * - We assume !(x <= y) means x > y, ignoring NaN. + * - We assume !(x <= y) means x > y, ignoring NaN, unless a nearby comment or identifier mentions NaN. * * - We assume integer arithmetic is exact, ignoring values above 2^53. * @@ -257,6 +257,24 @@ module RangeAnalysis { ) } + /** + * Holds if the given container has a comment or identifier mentioning `NaN`. + */ + predicate hasNaNIndicator(StmtContainer container) { + exists (Comment comment | + comment.getText().regexpMatch("(?s).*N[aA]N.*") and + comment.getFile() = container.getFile() and + ( + comment.getLocation().getStartLine() >= container.getLocation().getStartLine() and + comment.getLocation().getEndLine() <= container.getLocation().getEndLine() + or + comment.getNextToken() = container.getFirstToken() + )) + or + exists (Identifier id | id.getName() = "NaN" or id.getName() = "isNaN" | + id.getContainer() = container) + } + /** * Holds if `guard` asserts that the outcome of `A B + bias` is true, where `` is a comparison operator. */ @@ -266,6 +284,7 @@ module RangeAnalysis { ( guard.getOutcome() = true and operator = compare.getOperator() or + not hasNaNIndicator(guard.getContainer()) and guard.getOutcome() = false and operator = negateOperator(compare.getOperator()) ) ) diff --git a/javascript/ql/test/library-tests/RangeAnalysis/tst.js b/javascript/ql/test/library-tests/RangeAnalysis/tst.js index f89086711f6b..6dcdd8420457 100644 --- a/javascript/ql/test/library-tests/RangeAnalysis/tst.js +++ b/javascript/ql/test/library-tests/RangeAnalysis/tst.js @@ -50,3 +50,15 @@ function h(x, y) { if (x > y) {} // NOT OK - always true } } + +function nan(x) { + // This is a NaN comment. + if (x - 1 < x) {} // OK +} + +/** + * This is a NAN comment. + */ +function nan2(x) { + if (x - 1 < x) {} // OK +} From d813635f3e5af74df2783822de270ea83dad6389 Mon Sep 17 00:00:00 2001 From: Asger F Date: Thu, 25 Oct 2018 16:05:23 +0100 Subject: [PATCH 10/27] JS: Restrict constraint generation to relevant nodes --- .../src/semmle/javascript/RangeAnalysis.qll | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/javascript/ql/src/semmle/javascript/RangeAnalysis.qll b/javascript/ql/src/semmle/javascript/RangeAnalysis.qll index e944b82494e9..befc244b8acd 100644 --- a/javascript/ql/src/semmle/javascript/RangeAnalysis.qll +++ b/javascript/ql/src/semmle/javascript/RangeAnalysis.qll @@ -79,11 +79,30 @@ import javascript * Contains predicates for reasoning about the relative numeric value of expressions. */ module RangeAnalysis { + /** + * Holds if the given node is relevant for range analysis. + */ + private predicate isRelevant(DataFlow::Node node) { + node = any(Comparison cmp).getAnOperand().flow() + or + node = any(ConditionGuardNode guard).getTest().flow() + or + exists (DataFlow::Node succ | isRelevant(succ) | + succ = node.getASuccessor() + or + linearDefinitionStep(succ, node, _, _) + or + exists (BinaryExpr bin | bin instanceof AddExpr or bin instanceof SubExpr | + succ.asExpr() = bin and + bin.getAnOperand().flow() = node)) + } + /** * Holds if the given node has a unique data flow predecessor. */ pragma[noinline] private predicate hasUniquePredecessor(DataFlow::Node node) { + isRelevant(node) and strictcount(node.getAPredecessor()) = 1 } @@ -206,6 +225,7 @@ module RangeAnalysis { sign = sign1 * sign2 and bias = bias1 + sign1 * bias2) else ( + isRelevant(r) and root = r and sign = 1 and bias = 0 @@ -344,6 +364,7 @@ module RangeAnalysis { */ predicate binaryPhiNode(DataFlow::Node node, DataFlow::Node left, DataFlow::Node right) { exists (SsaPhiNode phi | node = DataFlow::ssaDefinitionNode(phi) | + isRelevant(node) and strictcount(phi.getAnInput()) = 2 and left = DataFlow::ssaDefinitionNode(phi.getAnInput()) and right = DataFlow::ssaDefinitionNode(phi.getAnInput()) and @@ -412,6 +433,7 @@ module RangeAnalysis { */ predicate constantEdge(ControlFlowNode cfg, DataFlow::Node a, int asign, DataFlow::Node b, int bsign, Bias c) { exists (NumberLiteral literal | cfg = literal | + isRelevant(literal.flow()) and literal.getIntValue() instanceof Bias and // avoid overflow a = literal.flow() and b = a and From 344bec38656f4e220cbd12bb4ccce5cf94b8f2ca Mon Sep 17 00:00:00 2001 From: Asger F Date: Fri, 21 Sep 2018 12:24:25 +0100 Subject: [PATCH 11/27] JS: Add UselessRangeCheck.ql --- .../ql/src/Statements/UselessRangeCheck.qhelp | 47 +++++++++++++++++++ .../ql/src/Statements/UselessRangeCheck.ql | 28 +++++++++++ .../Statements/examples/UselessRangeCheck.js | 12 +++++ .../examples/UselessRangeCheckGood.js | 8 ++++ .../UselessRangeCheck.expected | 1 + .../UselessRangeCheck/UselessRangeCheck.qlref | 1 + .../Statements/UselessRangeCheck/example.js | 12 +++++ .../UselessRangeCheck/exampleGood.js | 8 ++++ 8 files changed, 117 insertions(+) create mode 100644 javascript/ql/src/Statements/UselessRangeCheck.qhelp create mode 100644 javascript/ql/src/Statements/UselessRangeCheck.ql create mode 100644 javascript/ql/src/Statements/examples/UselessRangeCheck.js create mode 100644 javascript/ql/src/Statements/examples/UselessRangeCheckGood.js create mode 100644 javascript/ql/test/query-tests/Statements/UselessRangeCheck/UselessRangeCheck.expected create mode 100644 javascript/ql/test/query-tests/Statements/UselessRangeCheck/UselessRangeCheck.qlref create mode 100644 javascript/ql/test/query-tests/Statements/UselessRangeCheck/example.js create mode 100644 javascript/ql/test/query-tests/Statements/UselessRangeCheck/exampleGood.js diff --git a/javascript/ql/src/Statements/UselessRangeCheck.qhelp b/javascript/ql/src/Statements/UselessRangeCheck.qhelp new file mode 100644 index 000000000000..74a6168dd722 --- /dev/null +++ b/javascript/ql/src/Statements/UselessRangeCheck.qhelp @@ -0,0 +1,47 @@ + + + +

+If a condition always evaluates to true or always evaluates to false, this often indicates +incomplete code or a latent bug and should be examined carefully. +

+ +
+ + +

+Examine the surrounding code to determine why the condition is useless. If it is no +longer needed, remove it. +

+ +

+If the check is needed to guard against NaN values, insert a comment explaning the possibility of NaN. +

+ +
+ + +

+The following example finds the index of an element in a given slice of the array: +

+ + + +

+The condition i < end at the end is always false, however. The code can be clarified if the +redundant condition is removed: +

+ + + +
+ + + +
  • Mozilla Developer Network: Truthy, +Falsy.
  • + +
    +
    diff --git a/javascript/ql/src/Statements/UselessRangeCheck.ql b/javascript/ql/src/Statements/UselessRangeCheck.ql new file mode 100644 index 000000000000..f737110442f5 --- /dev/null +++ b/javascript/ql/src/Statements/UselessRangeCheck.ql @@ -0,0 +1,28 @@ +/** + * @name Useless range check + * @description If a range check always fails or always succeeds it is indicative of a bug. + * @kind problem + * @problem.severity warning + * @id js/useless-range-check + * @tags correctness + * @precision high + */ + +import javascript + +/** + * Gets the guard node with the opposite outcome of `guard`. + */ +ConditionGuardNode getOppositeGuard(ConditionGuardNode guard) { + result.getTest() = guard.getTest() and + result.getOutcome() = guard.getOutcome().booleanNot() +} + +from ConditionGuardNode guard +where RangeAnalysis::isContradictoryGuardNode(guard) + + // Do not report conditions that themselves are unreachable because of + // a prior contradiction. + and not RangeAnalysis::isContradictoryGuardNode(getOppositeGuard(guard)) + +select guard.getTest(), "The condition '" + guard.getTest() + "' is always " + guard.getOutcome().booleanNot() diff --git a/javascript/ql/src/Statements/examples/UselessRangeCheck.js b/javascript/ql/src/Statements/examples/UselessRangeCheck.js new file mode 100644 index 000000000000..53496ce2e4ef --- /dev/null +++ b/javascript/ql/src/Statements/examples/UselessRangeCheck.js @@ -0,0 +1,12 @@ +function findValue(values, x, start, end) { + let i; + for (i = start; i < end; ++i) { + if (values[i] === x) { + return i; + } + } + if (i < end) { + return i; + } + return -1; +} diff --git a/javascript/ql/src/Statements/examples/UselessRangeCheckGood.js b/javascript/ql/src/Statements/examples/UselessRangeCheckGood.js new file mode 100644 index 000000000000..6f86622c99b6 --- /dev/null +++ b/javascript/ql/src/Statements/examples/UselessRangeCheckGood.js @@ -0,0 +1,8 @@ +function findValue(values, x, start, end) { + for (let i = start; i < end; ++i) { + if (values[i] === x) { + return i; + } + } + return -1; +} diff --git a/javascript/ql/test/query-tests/Statements/UselessRangeCheck/UselessRangeCheck.expected b/javascript/ql/test/query-tests/Statements/UselessRangeCheck/UselessRangeCheck.expected new file mode 100644 index 000000000000..bdf5055e767c --- /dev/null +++ b/javascript/ql/test/query-tests/Statements/UselessRangeCheck/UselessRangeCheck.expected @@ -0,0 +1 @@ +| example.js:8:7:8:13 | i < end | The condition 'i < end' is always false | diff --git a/javascript/ql/test/query-tests/Statements/UselessRangeCheck/UselessRangeCheck.qlref b/javascript/ql/test/query-tests/Statements/UselessRangeCheck/UselessRangeCheck.qlref new file mode 100644 index 000000000000..f5d7a1ee7a40 --- /dev/null +++ b/javascript/ql/test/query-tests/Statements/UselessRangeCheck/UselessRangeCheck.qlref @@ -0,0 +1 @@ +Statements/UselessRangeCheck.ql diff --git a/javascript/ql/test/query-tests/Statements/UselessRangeCheck/example.js b/javascript/ql/test/query-tests/Statements/UselessRangeCheck/example.js new file mode 100644 index 000000000000..53496ce2e4ef --- /dev/null +++ b/javascript/ql/test/query-tests/Statements/UselessRangeCheck/example.js @@ -0,0 +1,12 @@ +function findValue(values, x, start, end) { + let i; + for (i = start; i < end; ++i) { + if (values[i] === x) { + return i; + } + } + if (i < end) { + return i; + } + return -1; +} diff --git a/javascript/ql/test/query-tests/Statements/UselessRangeCheck/exampleGood.js b/javascript/ql/test/query-tests/Statements/UselessRangeCheck/exampleGood.js new file mode 100644 index 000000000000..6f86622c99b6 --- /dev/null +++ b/javascript/ql/test/query-tests/Statements/UselessRangeCheck/exampleGood.js @@ -0,0 +1,8 @@ +function findValue(values, x, start, end) { + for (let i = start; i < end; ++i) { + if (values[i] === x) { + return i; + } + } + return -1; +} From 84ea4cf1d1db0223855809e42a6166b70d4c7afc Mon Sep 17 00:00:00 2001 From: Asger F Date: Thu, 1 Nov 2018 12:11:02 +0000 Subject: [PATCH 12/27] JS: manually reorder extendedEdge and negativeEdge --- .../src/semmle/javascript/RangeAnalysis.qll | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/javascript/ql/src/semmle/javascript/RangeAnalysis.qll b/javascript/ql/src/semmle/javascript/RangeAnalysis.qll index befc244b8acd..0079bd842f3c 100644 --- a/javascript/ql/src/semmle/javascript/RangeAnalysis.qll +++ b/javascript/ql/src/semmle/javascript/RangeAnalysis.qll @@ -507,12 +507,12 @@ module RangeAnalysis { * * This means negative-weight cycles (contradictions) can be detected using simple cycle detection. */ - private predicate extendedEdge(ControlFlowNode cfg, DataFlow::Node a, int asign, DataFlow::Node b, int bsign, Bias c, boolean sharp) { + private predicate extendedEdge(DataFlow::Node a, int asign, DataFlow::Node b, int bsign, Bias c, boolean sharp, ControlFlowNode cfg) { seedEdgeWithDual(cfg, a, asign, b, bsign, c, sharp) or exists (DataFlow::Node mid, int midx, ControlFlowNode cfg1, Bias c1, ControlFlowNode cfg2, Bias c2, boolean sharp1, boolean sharp2 | - extendedEdge(cfg1, a, asign, mid, midx, c1, sharp1) and - extendedEdge(cfg2, mid, midx, b, bsign, c2, sharp2) and + extendedEdge(a, asign, mid, midx, c1, sharp1, cfg1) and + extendedEdge(mid, midx, b, bsign, c2, sharp2, cfg2) and sharp = sharp1.booleanOr(sharp2) and c = wideningAddition(c1, sharp1, c2, sharp2) and // One of the two CFG nodes must dominate the other, and `cfg` must be bound to the dominated one. @@ -539,8 +539,8 @@ module RangeAnalysis { /** * Holds if there is a negative-weight edge from src to dst. */ - private predicate negativeEdge(ControlFlowNode cfg, DataFlow::Node a, int asign, DataFlow::Node b, int bsign) { - exists (int weight, boolean sharp | extendedEdge(cfg, a, asign, b, bsign, weight, sharp) | + private predicate negativeEdge(DataFlow::Node a, int asign, DataFlow::Node b, int bsign, ControlFlowNode cfg) { + exists (int weight, boolean sharp | extendedEdge(a, asign, b, bsign, weight, sharp, cfg) | weight = 0 and sharp = true // a strict "< 0" edge counts as negative or weight < 0) @@ -552,12 +552,12 @@ module RangeAnalysis { * The initial outgoing edge from `src` must be derived at `cfg`. */ pragma[noopt] - private predicate reachableByNegativeEdges(ControlFlowNode cfg, DataFlow::Node a, int asign, DataFlow::Node b, int bsign) { - negativeEdge(cfg, a, asign, b, bsign) + private predicate reachableByNegativeEdges(DataFlow::Node a, int asign, DataFlow::Node b, int bsign, ControlFlowNode cfg) { + negativeEdge(a, asign, b, bsign, cfg) or exists(DataFlow::Node mid, int midx, ControlFlowNode midcfg | - reachableByNegativeEdges(cfg, a, asign, mid, midx) and - negativeEdge(midcfg, mid, midx, b, bsign) and + reachableByNegativeEdges(a, asign, mid, midx, cfg) and + negativeEdge(mid, midx, b, bsign, midcfg) and exists (BasicBlock bb, int i, int j | bb.getNode(i) = midcfg and bb.getNode(j) = cfg and @@ -565,8 +565,8 @@ module RangeAnalysis { or // Same as above, but where CFG nodes are in different basic blocks exists(DataFlow::Node mid, int midx, ControlFlowNode midcfg, BasicBlock midBB, ReachableBasicBlock midRBB, BasicBlock cfgBB | - reachableByNegativeEdges(cfg, a, asign, mid, midx) and - negativeEdge(midcfg, mid, midx, b, bsign) and + reachableByNegativeEdges(a, asign, mid, midx, cfg) and + negativeEdge(mid, midx, b, bsign, midcfg) and midBB = midcfg.getBasicBlock() and midRBB = midBB.(ReachableBasicBlock) and cfgBB = cfg.getBasicBlock() and @@ -579,6 +579,6 @@ module RangeAnalysis { * opposite of the expected outcome. */ predicate isContradictoryGuardNode(ConditionGuardNode guard) { - exists (DataFlow::Node a, int asign | reachableByNegativeEdges(guard, a, asign, a, asign)) + exists (DataFlow::Node a, int asign | reachableByNegativeEdges(a, asign, a, asign, guard)) } } From 2d6bf0aff36cc6b1134484aad22dac117959863b Mon Sep 17 00:00:00 2001 From: Asger F Date: Thu, 1 Nov 2018 13:36:03 +0000 Subject: [PATCH 13/27] JS: improve join ordering in extendedEdge --- .../src/semmle/javascript/RangeAnalysis.qll | 61 ++++++++++++------- 1 file changed, 40 insertions(+), 21 deletions(-) diff --git a/javascript/ql/src/semmle/javascript/RangeAnalysis.qll b/javascript/ql/src/semmle/javascript/RangeAnalysis.qll index 0079bd842f3c..da6ef36dcb0a 100644 --- a/javascript/ql/src/semmle/javascript/RangeAnalysis.qll +++ b/javascript/ql/src/semmle/javascript/RangeAnalysis.qll @@ -507,33 +507,52 @@ module RangeAnalysis { * * This means negative-weight cycles (contradictions) can be detected using simple cycle detection. */ + pragma[noopt] private predicate extendedEdge(DataFlow::Node a, int asign, DataFlow::Node b, int bsign, Bias c, boolean sharp, ControlFlowNode cfg) { seedEdgeWithDual(cfg, a, asign, b, bsign, c, sharp) or - exists (DataFlow::Node mid, int midx, ControlFlowNode cfg1, Bias c1, ControlFlowNode cfg2, Bias c2, boolean sharp1, boolean sharp2 | + // One of the two CFG nodes must dominate the other, and `cfg` must be bound to the dominated one. + exists (ControlFlowNode cfg1, ControlFlowNode cfg2 | + // They are in the same basic block + extendedEdgeCandidate(a, asign, b, bsign, c, sharp, cfg1, cfg2) and + exists (BasicBlock bb, int i, int j | + bb.getNode(i) = cfg1 and + bb.getNode(j) = cfg2 and + if i < j then + cfg = cfg2 + else + cfg = cfg1) + or + // They are in different basic blocks + extendedEdgeCandidate(a, asign, b, bsign, c, sharp, cfg1, cfg2) and + exists (BasicBlock cfg1BB, ReachableBasicBlock cfg1RBB, BasicBlock cfg2BB, ReachableBasicBlock cfg2RBB | + cfg1BB = cfg1.getBasicBlock() and + cfg1RBB = cfg1BB.(ReachableBasicBlock) and + cfg2BB = cfg2.getBasicBlock() and + cfg2RBB = cfg2BB.(ReachableBasicBlock) and + ( + cfg1RBB.strictlyDominates(cfg2BB) and + cfg = cfg2 + or + cfg2RBB.strictlyDominates(cfg1RBB) and + cfg = cfg1 + )) + ) and + cfg instanceof ControlFlowNode + } + + /** + * Holds if an extended edge from `A` to `B` can potentially be generates from two edges, from `cfg1` and `cfg2`, respectively. + * + * This does not check for dominance between `cfg1` and `cfg2`. + */ + pragma[nomagic] + private predicate extendedEdgeCandidate(DataFlow::Node a, int asign, DataFlow::Node b, int bsign, Bias c, boolean sharp, ControlFlowNode cfg1, ControlFlowNode cfg2) { + exists (DataFlow::Node mid, int midx, Bias c1, Bias c2, boolean sharp1, boolean sharp2 | extendedEdge(a, asign, mid, midx, c1, sharp1, cfg1) and extendedEdge(mid, midx, b, bsign, c2, sharp2, cfg2) and sharp = sharp1.booleanOr(sharp2) and - c = wideningAddition(c1, sharp1, c2, sharp2) and - // One of the two CFG nodes must dominate the other, and `cfg` must be bound to the dominated one. - ( - // They are in the same basic block - exists (BasicBlock bb, int i, int j | - bb.getNode(i) = cfg1 and - bb.getNode(j) = cfg2 and - if i < j then - cfg = cfg2 - else - cfg = cfg1) - or - // They are in different basic blocks - cfg1.getBasicBlock().(ReachableBasicBlock).strictlyDominates(cfg2.getBasicBlock()) and - cfg = cfg2 - or - cfg2.getBasicBlock().(ReachableBasicBlock).strictlyDominates(cfg1.getBasicBlock()) and - cfg = cfg1 - ) - ) + c = wideningAddition(c1, sharp1, c2, sharp2)) } /** From 5283c6cd48af0d30edd6df224c147458f64505ad Mon Sep 17 00:00:00 2001 From: Asger F Date: Tue, 6 Nov 2018 11:47:18 +0000 Subject: [PATCH 14/27] JS: only warn about dead code --- .../ql/src/Statements/UselessRangeCheck.ql | 49 +++++++++++++++---- 1 file changed, 40 insertions(+), 9 deletions(-) diff --git a/javascript/ql/src/Statements/UselessRangeCheck.ql b/javascript/ql/src/Statements/UselessRangeCheck.ql index f737110442f5..c52a24c6341e 100644 --- a/javascript/ql/src/Statements/UselessRangeCheck.ql +++ b/javascript/ql/src/Statements/UselessRangeCheck.ql @@ -11,18 +11,49 @@ import javascript /** - * Gets the guard node with the opposite outcome of `guard`. + * Holds if there are any contradictory guard nodes in `container`. + * + * We use this to restrict reachability analysis to a small set of containers. */ -ConditionGuardNode getOppositeGuard(ConditionGuardNode guard) { - result.getTest() = guard.getTest() and - result.getOutcome() = guard.getOutcome().booleanNot() +predicate hasContradictoryGuardNodes(StmtContainer container) { + exists (ConditionGuardNode guard | + RangeAnalysis::isContradictoryGuardNode(guard) and + container = guard.getContainer()) } -from ConditionGuardNode guard -where RangeAnalysis::isContradictoryGuardNode(guard) +/** + * Holds if `block` is reachable and is in a container with contradictory guard nodes. + */ +predicate isReachable(BasicBlock block) { + exists (StmtContainer container | + hasContradictoryGuardNodes(container) and + block = container.getEntryBB()) + or + isReachable(block.getAPredecessor()) and + not RangeAnalysis::isContradictoryGuardNode(block.getANode()) +} + +/** + * Holds if `block` is unreachable, but could be reached if `guard` was not contradictory. + */ +predicate isBlockedByContradictoryGuardNodes(BasicBlock block, ConditionGuardNode guard) { + RangeAnalysis::isContradictoryGuardNode(guard) and + isReachable(block.getAPredecessor()) and // the guard itself is reachable + block = guard.getBasicBlock() + or + isBlockedByContradictoryGuardNodes(block.getAPredecessor(), guard) and + not isReachable(block) +} - // Do not report conditions that themselves are unreachable because of - // a prior contradiction. - and not RangeAnalysis::isContradictoryGuardNode(getOppositeGuard(guard)) +/** + * Holds if the given guard node is contradictory and causes an expression or statement to be unreachable. + */ +predicate isGuardNodeWithDeadCode(ConditionGuardNode guard) { + exists (BasicBlock block | + isBlockedByContradictoryGuardNodes(block, guard) and + block.getANode() instanceof ExprOrStmt) +} +from ConditionGuardNode guard +where isGuardNodeWithDeadCode(guard) select guard.getTest(), "The condition '" + guard.getTest() + "' is always " + guard.getOutcome().booleanNot() From 4a367d3fdb371d80c0cb69b64bc3b0f5f2ef2a67 Mon Sep 17 00:00:00 2001 From: Asger F Date: Fri, 9 Nov 2018 13:16:06 +0000 Subject: [PATCH 15/27] JS: more efficient encoding of unary constraints --- .../src/semmle/javascript/RangeAnalysis.qll | 48 ++++++++++++------- 1 file changed, 32 insertions(+), 16 deletions(-) diff --git a/javascript/ql/src/semmle/javascript/RangeAnalysis.qll b/javascript/ql/src/semmle/javascript/RangeAnalysis.qll index da6ef36dcb0a..bc2e8cbf39d5 100644 --- a/javascript/ql/src/semmle/javascript/RangeAnalysis.qll +++ b/javascript/ql/src/semmle/javascript/RangeAnalysis.qll @@ -424,34 +424,50 @@ module RangeAnalysis { } /** - * Holds constraints derived from `A = const`. + * Holds if a comparison implies that `A <= B + c`. * - * `A = c` is written to `A + A = 2c` which is then converted to `<=` and `>=`. - * - * A + A <= 2c becomes A <= -A + 2c - * A + A >= 2c becomes -A <= A - 2c + * Comparisons where one operand is really a constant are converted into a unary constraint. */ - predicate constantEdge(ControlFlowNode cfg, DataFlow::Node a, int asign, DataFlow::Node b, int bsign, Bias c) { - exists (NumberLiteral literal | cfg = literal | - isRelevant(literal.flow()) and - literal.getIntValue() instanceof Bias and // avoid overflow - a = literal.flow() and + predicate foldedComparisonEdge(ControlFlowNode cfg, DataFlow::Node a, int asign, DataFlow::Node b, int bsign, Bias c, boolean sharp) { + // A <= B + c (where A and B are not constants) + comparisonEdge(cfg, a, asign, b, bsign, c, sharp) and + not exists(a.asExpr().getIntValue()) and + not exists(b.asExpr().getIntValue()) + or + // A - k <= c becomes A - (-A) <= 2*(k + c) + exists (DataFlow::Node k, int ksign, Bias kbias, Bias value | + comparisonEdge(cfg, a, asign, k, ksign, kbias, sharp) and + value = k.asExpr().getIntValue() and b = a and - (asign = 1 or asign = -1) and bsign = -asign and - c = literal.getIntValue() * 2 * asign) + c = 2 * (value * ksign + kbias)) + or + // k - A <= c becomes -A - A <= 2(-k + c) + exists (DataFlow::Node k, int ksign, Bias kbias, Bias value | + comparisonEdge(cfg, k, ksign, b, bsign, kbias, sharp) and + value = k.asExpr().getIntValue() and + a = b and + asign = -bsign and + c = 2 * (-value * ksign + kbias)) + or + // For completeness, generate a contradictory constraint for trivially false conditions. + exists (DataFlow::Node k, int ksign, Bias bias, int avalue, int kvalue | + comparisonEdge(cfg, a, asign, k, ksign, bias, sharp) and + avalue = a.asExpr().getIntValue() * asign and + kvalue = b.asExpr().getIntValue() * bsign and + (avalue < kvalue + bias or sharp = true and avalue = kvalue + bias) and + a = b and + asign = bsign and + c = -1) } /** * The set of initial edges including those from dual constraints. */ predicate seedEdge(ControlFlowNode cfg, DataFlow::Node a, int asign, DataFlow::Node b, int bsign, Bias c, boolean sharp) { - // A <= B + c - comparisonEdge(cfg, a, asign, b, bsign, c, sharp) + foldedComparisonEdge(cfg, a, asign, b, bsign, c, sharp) or phiEdge(cfg, a, asign, b, bsign, c) and sharp = false - or - constantEdge(cfg, a, asign, b, bsign, c) and sharp = false } private predicate seedEdgeWithDual(ControlFlowNode cfg, DataFlow::Node a, int asign, DataFlow::Node b, int bsign, Bias c, boolean sharp) { From f3020f776e5b17356e31fd874c9524c42e691441 Mon Sep 17 00:00:00 2001 From: Asger F Date: Mon, 12 Nov 2018 12:26:06 +0000 Subject: [PATCH 16/27] JS: avoid extending self-edges --- javascript/ql/src/semmle/javascript/RangeAnalysis.qll | 2 ++ 1 file changed, 2 insertions(+) diff --git a/javascript/ql/src/semmle/javascript/RangeAnalysis.qll b/javascript/ql/src/semmle/javascript/RangeAnalysis.qll index bc2e8cbf39d5..e16731a6132e 100644 --- a/javascript/ql/src/semmle/javascript/RangeAnalysis.qll +++ b/javascript/ql/src/semmle/javascript/RangeAnalysis.qll @@ -567,6 +567,8 @@ module RangeAnalysis { exists (DataFlow::Node mid, int midx, Bias c1, Bias c2, boolean sharp1, boolean sharp2 | extendedEdge(a, asign, mid, midx, c1, sharp1, cfg1) and extendedEdge(mid, midx, b, bsign, c2, sharp2, cfg2) and + (a != mid or asign != midx) and + (b != mid or bsign != midx) and sharp = sharp1.booleanOr(sharp2) and c = wideningAddition(c1, sharp1, c2, sharp2)) } From 76a69f4ff2a4bd8ad1ed34466270deaeaebafd12 Mon Sep 17 00:00:00 2001 From: Asger F Date: Tue, 13 Nov 2018 12:44:29 +0000 Subject: [PATCH 17/27] JS: address review comments --- .../ql/src/Statements/UselessRangeCheck.qhelp | 4 +-- .../src/semmle/javascript/RangeAnalysis.qll | 32 ++++++++++++------- 2 files changed, 23 insertions(+), 13 deletions(-) diff --git a/javascript/ql/src/Statements/UselessRangeCheck.qhelp b/javascript/ql/src/Statements/UselessRangeCheck.qhelp index 74a6168dd722..c9f933ef7872 100644 --- a/javascript/ql/src/Statements/UselessRangeCheck.qhelp +++ b/javascript/ql/src/Statements/UselessRangeCheck.qhelp @@ -17,7 +17,7 @@ longer needed, remove it.

    -If the check is needed to guard against NaN values, insert a comment explaning the possibility of NaN. +If the check is needed to guard against NaN values, insert a comment explaning the possibility of NaN.

    @@ -30,7 +30,7 @@ The following example finds the index of an element in a given slice of the arra

    -The condition i < end at the end is always false, however. The code can be clarified if the +The condition i < end at the end is always false, however. The code can be clarified if the redundant condition is removed:

    diff --git a/javascript/ql/src/semmle/javascript/RangeAnalysis.qll b/javascript/ql/src/semmle/javascript/RangeAnalysis.qll index e16731a6132e..229d3bc97e4a 100644 --- a/javascript/ql/src/semmle/javascript/RangeAnalysis.qll +++ b/javascript/ql/src/semmle/javascript/RangeAnalysis.qll @@ -161,31 +161,35 @@ module RangeAnalysis { not exists(r.asExpr().getIntValue()) and ( exists (AddExpr expr | r.asExpr() = expr | + // r = root + k root = expr.getLeftOperand().flow() and bias = expr.getRightOperand().getIntValue() and - sign = 1) - or - exists (AddExpr expr | r.asExpr() = expr | + sign = 1 + or + // r = k + root bias = expr.getLeftOperand().getIntValue() and root = expr.getRightOperand().flow() and sign = 1) or exists (SubExpr expr | r.asExpr() = expr | + // r = root - k root = expr.getLeftOperand().flow() and bias = -expr.getRightOperand().getIntValue() and - sign = 1) - or - exists (SubExpr expr | r.asExpr() = expr | + sign = 1 + or + // r = k - root bias = expr.getLeftOperand().getIntValue() and root = expr.getRightOperand().flow() and sign = -1) or exists (NegExpr expr | r.asExpr() = expr | + // r = -root root = expr.getOperand().flow() and bias = 0 and sign = -1) or exists (UpdateExpr update | r = updateExprResult(update) | + // r = ++root root = update.getOperand().flow() and sign = 1 and if update instanceof IncExpr then @@ -194,6 +198,7 @@ module RangeAnalysis { bias = -1) or exists (UpdateExpr update | r.asExpr() = update | // Return value of x++ is just x (coerced to an int) + // r = root++ root = update.getOperand().flow() and not update.isPrefix() and sign = 1 and @@ -203,9 +208,11 @@ module RangeAnalysis { root = assign.getLhs().flow() and sign = 1 and ( + // r = root += k assign instanceof AssignAddExpr and bias = assign.getRhs().getIntValue() or + // r = root -= k assign instanceof AssignSubExpr and bias = -assign.getRhs().getIntValue() )) @@ -220,8 +227,11 @@ module RangeAnalysis { linearDefinition(r.getAPredecessor(), root, sign, bias) else if linearDefinitionStep(r, _, _, _) then exists (DataFlow::Node pred, int sign1, int bias1, int sign2, int bias2 | + // r = pred * sign1 + bias1 linearDefinitionStep(r, pred, sign1, bias1) and + // pred = root * sign2 + bias2 linearDefinition(pred, root, sign2, bias2) and + // r = (root * sign2 + bias2) * sign1 + bias1 sign = sign1 * sign2 and bias = bias1 + sign1 * bias2) else ( @@ -242,11 +252,13 @@ module RangeAnalysis { none() // do not model constants as sums else ( exists (AddExpr add, int bias1, int bias2 | r.asExpr() = add | + // r = r1 + r2 linearDefinition(add.getLeftOperand().flow(), xroot, xsign, bias1) and linearDefinition(add.getRightOperand().flow(), yroot, ysign, bias2) and bias = bias1 + bias2) or exists (SubExpr sub, int bias1, int bias2 | r.asExpr() = sub | + // r = r1 - r2 linearDefinition(sub.getLeftOperand().flow(), xroot, xsign, bias1) and linearDefinition(sub.getRightOperand().flow(), yroot, -ysign, -bias2) and // Negate right-hand operand bias = bias1 + bias2) @@ -256,7 +268,8 @@ module RangeAnalysis { } /** - * Holds if the given comparison can be modelled as `A B + bias` where `` is the comparison operator. + * Holds if the given comparison can be modelled as `A B + bias` where `` is the comparison operator, + * and `A` is `a * asign` and likewise `B` is `b * bsign`. */ predicate linearComparison(Comparison comparison, DataFlow::Node a, int asign, DataFlow::Node b, int bsign, Bias bias) { exists(Expr left, Expr right, int bias1, int bias2 | left = comparison.getLeftOperand() and right = comparison.getRightOperand() | @@ -315,13 +328,10 @@ module RangeAnalysis { */ private string negateOperator(string operator) { operator = "==" and result = "!=" or - operator = "!=" and result = "==" or operator = "===" and result = "!==" or - operator = "!==" and result = "===" or operator = "<" and result = ">=" or - operator = "<=" and result = ">" or operator = ">" and result = "<=" or - operator = ">=" and result = "<" + operator = negateOperator(result) } /** From 287020929919710df5702bfda292f6a48bdad7f1 Mon Sep 17 00:00:00 2001 From: Asger F Date: Thu, 15 Nov 2018 16:49:48 +0000 Subject: [PATCH 18/27] JS: fix links in qhelp file --- javascript/ql/src/Statements/UselessRangeCheck.qhelp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/javascript/ql/src/Statements/UselessRangeCheck.qhelp b/javascript/ql/src/Statements/UselessRangeCheck.qhelp index c9f933ef7872..6c35fdd2848d 100644 --- a/javascript/ql/src/Statements/UselessRangeCheck.qhelp +++ b/javascript/ql/src/Statements/UselessRangeCheck.qhelp @@ -27,14 +27,14 @@ If the check is needed to guard against NaN values, insert a commen The following example finds the index of an element in a given slice of the array:

    - +

    The condition i < end at the end is always false, however. The code can be clarified if the redundant condition is removed:

    - + From 2e65f6b66022b20d708d5b328f15bb2329f121ce Mon Sep 17 00:00:00 2001 From: Asger F Date: Thu, 15 Nov 2018 16:53:34 +0000 Subject: [PATCH 19/27] JS: address some style comments --- javascript/ql/src/Statements/UselessRangeCheck.qhelp | 4 ++-- javascript/ql/src/Statements/UselessRangeCheck.ql | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/javascript/ql/src/Statements/UselessRangeCheck.qhelp b/javascript/ql/src/Statements/UselessRangeCheck.qhelp index 6c35fdd2848d..8bac3f99818f 100644 --- a/javascript/ql/src/Statements/UselessRangeCheck.qhelp +++ b/javascript/ql/src/Statements/UselessRangeCheck.qhelp @@ -5,14 +5,14 @@

    If a condition always evaluates to true or always evaluates to false, this often indicates -incomplete code or a latent bug and should be examined carefully. +incomplete code or a latent bug, and it should be examined carefully.

    -Examine the surrounding code to determine why the condition is useless. If it is no +Examine the surrounding code to determine why the condition is redundant. If it is no longer needed, remove it.

    diff --git a/javascript/ql/src/Statements/UselessRangeCheck.ql b/javascript/ql/src/Statements/UselessRangeCheck.ql index c52a24c6341e..c124b5e3a9ad 100644 --- a/javascript/ql/src/Statements/UselessRangeCheck.ql +++ b/javascript/ql/src/Statements/UselessRangeCheck.ql @@ -1,6 +1,6 @@ /** * @name Useless range check - * @description If a range check always fails or always succeeds it is indicative of a bug. + * @description If a range check always fails or always succeeds, it is indicative of a bug. * @kind problem * @problem.severity warning * @id js/useless-range-check From 477be260f35b64d37fb73aa1b5262d2f23b6c9d7 Mon Sep 17 00:00:00 2001 From: Asger F Date: Thu, 15 Nov 2018 17:08:34 +0000 Subject: [PATCH 20/27] JS: rename UselessRangeCheck -> UselessComparisonTest --- .../{UselessRangeCheck.qhelp => UselessComparisonTest.qhelp} | 4 ++-- .../{UselessRangeCheck.ql => UselessComparisonTest.ql} | 5 +++-- .../{UselessRangeCheck.js => UselessComparisonTest.js} | 0 ...UselessRangeCheckGood.js => UselessComparisonTestGood.js} | 0 .../UselessComparisonTest.expected} | 0 .../UselessComparisonTest/UselessComparisonTest.qlref | 1 + .../{UselessRangeCheck => UselessComparisonTest}/example.js | 0 .../exampleGood.js | 0 .../Statements/UselessRangeCheck/UselessRangeCheck.qlref | 1 - 9 files changed, 6 insertions(+), 5 deletions(-) rename javascript/ql/src/Statements/{UselessRangeCheck.qhelp => UselessComparisonTest.qhelp} (90%) rename javascript/ql/src/Statements/{UselessRangeCheck.ql => UselessComparisonTest.ql} (91%) rename javascript/ql/src/Statements/examples/{UselessRangeCheck.js => UselessComparisonTest.js} (100%) rename javascript/ql/src/Statements/examples/{UselessRangeCheckGood.js => UselessComparisonTestGood.js} (100%) rename javascript/ql/test/query-tests/Statements/{UselessRangeCheck/UselessRangeCheck.expected => UselessComparisonTest/UselessComparisonTest.expected} (100%) create mode 100644 javascript/ql/test/query-tests/Statements/UselessComparisonTest/UselessComparisonTest.qlref rename javascript/ql/test/query-tests/Statements/{UselessRangeCheck => UselessComparisonTest}/example.js (100%) rename javascript/ql/test/query-tests/Statements/{UselessRangeCheck => UselessComparisonTest}/exampleGood.js (100%) delete mode 100644 javascript/ql/test/query-tests/Statements/UselessRangeCheck/UselessRangeCheck.qlref diff --git a/javascript/ql/src/Statements/UselessRangeCheck.qhelp b/javascript/ql/src/Statements/UselessComparisonTest.qhelp similarity index 90% rename from javascript/ql/src/Statements/UselessRangeCheck.qhelp rename to javascript/ql/src/Statements/UselessComparisonTest.qhelp index 8bac3f99818f..27483f5516a0 100644 --- a/javascript/ql/src/Statements/UselessRangeCheck.qhelp +++ b/javascript/ql/src/Statements/UselessComparisonTest.qhelp @@ -27,14 +27,14 @@ If the check is needed to guard against NaN values, insert a commen The following example finds the index of an element in a given slice of the array:

    - +

    The condition i < end at the end is always false, however. The code can be clarified if the redundant condition is removed:

    - + diff --git a/javascript/ql/src/Statements/UselessRangeCheck.ql b/javascript/ql/src/Statements/UselessComparisonTest.ql similarity index 91% rename from javascript/ql/src/Statements/UselessRangeCheck.ql rename to javascript/ql/src/Statements/UselessComparisonTest.ql index c124b5e3a9ad..6a4eef8c631e 100644 --- a/javascript/ql/src/Statements/UselessRangeCheck.ql +++ b/javascript/ql/src/Statements/UselessComparisonTest.ql @@ -1,6 +1,7 @@ /** - * @name Useless range check - * @description If a range check always fails or always succeeds, it is indicative of a bug. + * @name Useless comparison test + * @description A comparison that always evaluates to true or always evaluates to false may + * indicate faulty logic and dead code. * @kind problem * @problem.severity warning * @id js/useless-range-check diff --git a/javascript/ql/src/Statements/examples/UselessRangeCheck.js b/javascript/ql/src/Statements/examples/UselessComparisonTest.js similarity index 100% rename from javascript/ql/src/Statements/examples/UselessRangeCheck.js rename to javascript/ql/src/Statements/examples/UselessComparisonTest.js diff --git a/javascript/ql/src/Statements/examples/UselessRangeCheckGood.js b/javascript/ql/src/Statements/examples/UselessComparisonTestGood.js similarity index 100% rename from javascript/ql/src/Statements/examples/UselessRangeCheckGood.js rename to javascript/ql/src/Statements/examples/UselessComparisonTestGood.js diff --git a/javascript/ql/test/query-tests/Statements/UselessRangeCheck/UselessRangeCheck.expected b/javascript/ql/test/query-tests/Statements/UselessComparisonTest/UselessComparisonTest.expected similarity index 100% rename from javascript/ql/test/query-tests/Statements/UselessRangeCheck/UselessRangeCheck.expected rename to javascript/ql/test/query-tests/Statements/UselessComparisonTest/UselessComparisonTest.expected diff --git a/javascript/ql/test/query-tests/Statements/UselessComparisonTest/UselessComparisonTest.qlref b/javascript/ql/test/query-tests/Statements/UselessComparisonTest/UselessComparisonTest.qlref new file mode 100644 index 000000000000..ade7e7d0607e --- /dev/null +++ b/javascript/ql/test/query-tests/Statements/UselessComparisonTest/UselessComparisonTest.qlref @@ -0,0 +1 @@ +Statements/UselessComparisonTest.ql diff --git a/javascript/ql/test/query-tests/Statements/UselessRangeCheck/example.js b/javascript/ql/test/query-tests/Statements/UselessComparisonTest/example.js similarity index 100% rename from javascript/ql/test/query-tests/Statements/UselessRangeCheck/example.js rename to javascript/ql/test/query-tests/Statements/UselessComparisonTest/example.js diff --git a/javascript/ql/test/query-tests/Statements/UselessRangeCheck/exampleGood.js b/javascript/ql/test/query-tests/Statements/UselessComparisonTest/exampleGood.js similarity index 100% rename from javascript/ql/test/query-tests/Statements/UselessRangeCheck/exampleGood.js rename to javascript/ql/test/query-tests/Statements/UselessComparisonTest/exampleGood.js diff --git a/javascript/ql/test/query-tests/Statements/UselessRangeCheck/UselessRangeCheck.qlref b/javascript/ql/test/query-tests/Statements/UselessRangeCheck/UselessRangeCheck.qlref deleted file mode 100644 index f5d7a1ee7a40..000000000000 --- a/javascript/ql/test/query-tests/Statements/UselessRangeCheck/UselessRangeCheck.qlref +++ /dev/null @@ -1 +0,0 @@ -Statements/UselessRangeCheck.ql From 6d7ac885ec43092df6f561dfde253b568c6be3f0 Mon Sep 17 00:00:00 2001 From: Asger F Date: Tue, 20 Nov 2018 11:47:17 +0000 Subject: [PATCH 21/27] JS: add to correctness-more suite --- javascript/config/suites/javascript/correctness-more | 1 + 1 file changed, 1 insertion(+) diff --git a/javascript/config/suites/javascript/correctness-more b/javascript/config/suites/javascript/correctness-more index 10aa1dcf7967..2e57bcd7327b 100644 --- a/javascript/config/suites/javascript/correctness-more +++ b/javascript/config/suites/javascript/correctness-more @@ -15,3 +15,4 @@ + semmlecode-javascript-queries/Statements/ReturnOutsideFunction.ql: /Correctness/Statements + semmlecode-javascript-queries/Statements/SuspiciousUnusedLoopIterationVariable.ql: /Correctness/Statements + semmlecode-javascript-queries/Statements/UselessConditional.ql: /Correctness/Statements ++ semmlecode-javascript-queries/Statements/UselessComparisonTest.ql: /Correctness/Statements From 2c51f86f1ba6b671e43a76c8315b56adf2ae041a Mon Sep 17 00:00:00 2001 From: Asger F Date: Wed, 21 Nov 2018 16:42:37 +0000 Subject: [PATCH 22/27] JS: avoid joining on =0 --- javascript/ql/src/semmle/javascript/RangeAnalysis.qll | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/javascript/ql/src/semmle/javascript/RangeAnalysis.qll b/javascript/ql/src/semmle/javascript/RangeAnalysis.qll index 229d3bc97e4a..ec848209ac05 100644 --- a/javascript/ql/src/semmle/javascript/RangeAnalysis.qll +++ b/javascript/ql/src/semmle/javascript/RangeAnalysis.qll @@ -497,9 +497,9 @@ module RangeAnalysis { (x < 0 or x = 0 and sharpx = true) and (y > 0 or y = 0 and sharpy = false) and ( - x = 0 + x <= 0 and x >= 0 or - y = 0 + y <= 0 and y >= 0 or // If non-zero, check that the values are within a factor 16 of each other x.abs().bitShiftRight(4) < y.abs() and From 8fd3a417c253e96de1a89dce3e75f2f91bb89513 Mon Sep 17 00:00:00 2001 From: Asger F Date: Wed, 28 Nov 2018 09:47:50 +0000 Subject: [PATCH 23/27] JS: address comments --- javascript/ql/src/Statements/UselessComparisonTest.ql | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/javascript/ql/src/Statements/UselessComparisonTest.ql b/javascript/ql/src/Statements/UselessComparisonTest.ql index 6a4eef8c631e..419f02a000c3 100644 --- a/javascript/ql/src/Statements/UselessComparisonTest.ql +++ b/javascript/ql/src/Statements/UselessComparisonTest.ql @@ -4,7 +4,7 @@ * indicate faulty logic and dead code. * @kind problem * @problem.severity warning - * @id js/useless-range-check + * @id js/useless-comparison-test * @tags correctness * @precision high */ @@ -57,4 +57,4 @@ predicate isGuardNodeWithDeadCode(ConditionGuardNode guard) { from ConditionGuardNode guard where isGuardNodeWithDeadCode(guard) -select guard.getTest(), "The condition '" + guard.getTest() + "' is always " + guard.getOutcome().booleanNot() +select guard.getTest(), "The condition '" + guard.getTest() + "' is always " + guard.getOutcome().booleanNot() + "." From d69e584cc2b98a25ed9069f16566c43d4b043945 Mon Sep 17 00:00:00 2001 From: Asger F Date: Wed, 28 Nov 2018 10:21:43 +0000 Subject: [PATCH 24/27] JS: fix bug in foldedComparisonEdge --- javascript/ql/src/semmle/javascript/RangeAnalysis.qll | 4 ++-- javascript/ql/test/library-tests/RangeAnalysis/length.js | 3 +++ .../UselessComparisonTest/UselessComparisonTest.expected | 4 +++- .../query-tests/Statements/UselessComparisonTest/constant.js | 4 ++++ 4 files changed, 12 insertions(+), 3 deletions(-) create mode 100644 javascript/ql/test/library-tests/RangeAnalysis/length.js create mode 100644 javascript/ql/test/query-tests/Statements/UselessComparisonTest/constant.js diff --git a/javascript/ql/src/semmle/javascript/RangeAnalysis.qll b/javascript/ql/src/semmle/javascript/RangeAnalysis.qll index ec848209ac05..28ccbf2d0caa 100644 --- a/javascript/ql/src/semmle/javascript/RangeAnalysis.qll +++ b/javascript/ql/src/semmle/javascript/RangeAnalysis.qll @@ -464,8 +464,8 @@ module RangeAnalysis { exists (DataFlow::Node k, int ksign, Bias bias, int avalue, int kvalue | comparisonEdge(cfg, a, asign, k, ksign, bias, sharp) and avalue = a.asExpr().getIntValue() * asign and - kvalue = b.asExpr().getIntValue() * bsign and - (avalue < kvalue + bias or sharp = true and avalue = kvalue + bias) and + kvalue = k.asExpr().getIntValue() * ksign and + (avalue > kvalue + bias or sharp = true and avalue = kvalue + bias) and a = b and asign = bsign and c = -1) diff --git a/javascript/ql/test/library-tests/RangeAnalysis/length.js b/javascript/ql/test/library-tests/RangeAnalysis/length.js new file mode 100644 index 000000000000..41835c245b17 --- /dev/null +++ b/javascript/ql/test/library-tests/RangeAnalysis/length.js @@ -0,0 +1,3 @@ +function f(arr) { + if (arr.length > 2) {} // OK +} \ No newline at end of file diff --git a/javascript/ql/test/query-tests/Statements/UselessComparisonTest/UselessComparisonTest.expected b/javascript/ql/test/query-tests/Statements/UselessComparisonTest/UselessComparisonTest.expected index bdf5055e767c..edb4ea205bc6 100644 --- a/javascript/ql/test/query-tests/Statements/UselessComparisonTest/UselessComparisonTest.expected +++ b/javascript/ql/test/query-tests/Statements/UselessComparisonTest/UselessComparisonTest.expected @@ -1 +1,3 @@ -| example.js:8:7:8:13 | i < end | The condition 'i < end' is always false | +| constant.js:2:7:2:11 | 1 > 2 | The condition '1 > 2' is always false. | +| constant.js:3:7:3:11 | 1 > 0 | The condition '1 > 0' is always true. | +| example.js:8:7:8:13 | i < end | The condition 'i < end' is always false. | diff --git a/javascript/ql/test/query-tests/Statements/UselessComparisonTest/constant.js b/javascript/ql/test/query-tests/Statements/UselessComparisonTest/constant.js new file mode 100644 index 000000000000..aa5ee0e1fb5b --- /dev/null +++ b/javascript/ql/test/query-tests/Statements/UselessComparisonTest/constant.js @@ -0,0 +1,4 @@ +function f() { + if (1 > 2) {} else {} // NOT OK - always false + if (1 > 0) {} else {} // NOT OK - always true +} From 959776b77537260cefd1d98b598a38cb95970cdc Mon Sep 17 00:00:00 2001 From: Asger F Date: Tue, 27 Nov 2018 16:36:09 +0000 Subject: [PATCH 25/27] JS: add test case --- .../ql/test/library-tests/RangeAnalysis/expressions.js | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 javascript/ql/test/library-tests/RangeAnalysis/expressions.js diff --git a/javascript/ql/test/library-tests/RangeAnalysis/expressions.js b/javascript/ql/test/library-tests/RangeAnalysis/expressions.js new file mode 100644 index 000000000000..ec25286b1153 --- /dev/null +++ b/javascript/ql/test/library-tests/RangeAnalysis/expressions.js @@ -0,0 +1,9 @@ +function f(arr) { + if (arr.length > 2) {} // OK + + let x = arr.length || 0; + if (x > 2) {} // OK + + let y = arr.length && 3; + if (y > 2) {} // OK +} From b2a82ae598b2f6f5f65bcb21db885eaf13baa450 Mon Sep 17 00:00:00 2001 From: Asger F Date: Thu, 15 Nov 2018 17:08:58 +0000 Subject: [PATCH 26/27] JS: add 1.20 change note --- change-notes/1.20/analysis-javascript.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 change-notes/1.20/analysis-javascript.md diff --git a/change-notes/1.20/analysis-javascript.md b/change-notes/1.20/analysis-javascript.md new file mode 100644 index 000000000000..3d8a0022f122 --- /dev/null +++ b/change-notes/1.20/analysis-javascript.md @@ -0,0 +1,24 @@ +# Improvements to JavaScript analysis + +## General improvements + +* TODO + +## New queries + +| **Query** | **Tags** | **Purpose** | +|-----------------------------------------------|------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Useless comparison test | correctness | Highlight code that is unreachable due to a numeric comparison that is always true or alway false. | + + +## Changes to existing queries + +| **Query** | **Expected impact** | **Change** | +|--------------------------------|----------------------------|----------------------------------------------| +| todo | | | + + +## Changes to QL libraries + +* TODO + From d4023fe95a0038c150c6c24fbff9cd2361546662 Mon Sep 17 00:00:00 2001 From: Asger F Date: Thu, 29 Nov 2018 11:37:38 +0000 Subject: [PATCH 27/27] JS: address review --- change-notes/1.20/analysis-javascript.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/change-notes/1.20/analysis-javascript.md b/change-notes/1.20/analysis-javascript.md index 3d8a0022f122..a4687323110f 100644 --- a/change-notes/1.20/analysis-javascript.md +++ b/change-notes/1.20/analysis-javascript.md @@ -8,7 +8,7 @@ | **Query** | **Tags** | **Purpose** | |-----------------------------------------------|------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| Useless comparison test | correctness | Highlight code that is unreachable due to a numeric comparison that is always true or alway false. | +| Useless comparison test | correctness | Highlights code that is unreachable due to a numeric comparison that is always true or always false. | ## Changes to existing queries