Skip to content

C++: Handle switch statements in the guards library #15941

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Mar 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
category: feature
---
* Added a predicate `GuardCondition.valueControls` to query whether a basic block is guarded by a particular `case` of a `switch` statement.
158 changes: 120 additions & 38 deletions cpp/ql/lib/semmle/code/cpp/controlflow/IRGuards.qll
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,44 @@ private predicate isUnreachedBlock(IRBlock block) {
block.getFirstInstruction() instanceof UnreachedInstruction
}

private newtype TAbstractValue =
TBooleanValue(boolean b) { b = true or b = false } or
TMatchValue(CaseEdge c)

/**
* An abstract value. This is either a boolean value, or a `switch` case.
*/
abstract class AbstractValue extends TAbstractValue {
/** Gets an abstract value that represents the dual of this value, if any. */
abstract AbstractValue getDualValue();

/** Gets a textual representation of this abstract value. */
abstract string toString();
}

/** A Boolean value. */
class BooleanValue extends AbstractValue, TBooleanValue {
/** Gets the underlying Boolean value. */
boolean getValue() { this = TBooleanValue(result) }

override BooleanValue getDualValue() { result.getValue() = this.getValue().booleanNot() }

override string toString() { result = this.getValue().toString() }
}

/** A value that represents a match against a specific `switch` case. */
class MatchValue extends AbstractValue, TMatchValue {
/** Gets the case. */
CaseEdge getCase() { this = TMatchValue(result) }

override MatchValue getDualValue() {
// A `MatchValue` has no dual.
none()
}

override string toString() { result = this.getCase().toString() }
}

/**
* A Boolean condition in the AST that guards one or more basic blocks. This includes
* operands of logical operators but not switch statements.
Expand All @@ -34,6 +72,15 @@ class GuardCondition extends Expr {
this.(BinaryLogicalOperation).getAnOperand() instanceof GuardCondition
}

/**
* Holds if this condition controls `controlled`, meaning that `controlled` is only
* entered if the value of this condition is `v`.
*
* For details on what "controls" mean, see the QLDoc for `controls`.
*/
cached
predicate valueControls(BasicBlock controlled, AbstractValue v) { none() }

/**
* Holds if this condition controls `controlled`, meaning that `controlled` is only
* entered if the value of this condition is `testIsTrue`.
Expand Down Expand Up @@ -61,7 +108,9 @@ class GuardCondition extends Expr {
* true (for `&&`) or false (for `||`) branch.
*/
cached
predicate controls(BasicBlock controlled, boolean testIsTrue) { none() }
final predicate controls(BasicBlock controlled, boolean testIsTrue) {
this.valueControls(controlled, any(BooleanValue bv | bv.getValue() = testIsTrue))
}

/** Holds if (determined by this guard) `left < right + k` evaluates to `isLessThan` if this expression evaluates to `testIsTrue`. */
cached
Expand Down Expand Up @@ -98,13 +147,13 @@ private class GuardConditionFromBinaryLogicalOperator extends GuardCondition {
this.(BinaryLogicalOperation).getAnOperand() instanceof GuardCondition
}

override predicate controls(BasicBlock controlled, boolean testIsTrue) {
override predicate valueControls(BasicBlock controlled, AbstractValue v) {
exists(BinaryLogicalOperation binop, GuardCondition lhs, GuardCondition rhs |
this = binop and
lhs = binop.getLeftOperand() and
rhs = binop.getRightOperand() and
lhs.controls(controlled, testIsTrue) and
rhs.controls(controlled, testIsTrue)
lhs.valueControls(controlled, v) and
rhs.valueControls(controlled, v)
)
}

Expand Down Expand Up @@ -146,10 +195,10 @@ private class GuardConditionFromIR extends GuardCondition {

GuardConditionFromIR() { this = ir.getUnconvertedResultExpression() }

override predicate controls(BasicBlock controlled, boolean testIsTrue) {
override predicate valueControls(BasicBlock controlled, AbstractValue v) {
// This condition must determine the flow of control; that is, this
// node must be a top-level condition.
this.controlsBlock(controlled, testIsTrue)
this.controlsBlock(controlled, v)
}

/** Holds if (determined by this guard) `left < right + k` evaluates to `isLessThan` if this expression evaluates to `testIsTrue`. */
Expand Down Expand Up @@ -198,13 +247,13 @@ private class GuardConditionFromIR extends GuardCondition {

/**
* Holds if this condition controls `block`, meaning that `block` is only
* entered if the value of this condition is `testIsTrue`. This helper
* entered if the value of this condition is `v`. This helper
* predicate does not necessarily hold for binary logical operations like
* `&&` and `||`. See the detailed explanation on predicate `controls`.
*/
private predicate controlsBlock(BasicBlock controlled, boolean testIsTrue) {
private predicate controlsBlock(BasicBlock controlled, AbstractValue v) {
exists(IRBlock irb |
ir.controls(irb, testIsTrue) and
ir.valueControls(irb, v) and
nonExcludedIRAndBasicBlock(irb, controlled) and
not isUnreachedBlock(irb)
)
Expand Down Expand Up @@ -249,10 +298,28 @@ private predicate nonExcludedIRAndBasicBlock(IRBlock irb, BasicBlock controlled)
*/
cached
class IRGuardCondition extends Instruction {
ConditionalBranchInstruction branch;
Instruction branch;

cached
IRGuardCondition() { branch = get_branch_for_condition(this) }
IRGuardCondition() { branch = getBranchForCondition(this) }

/**
* Holds if this condition controls `controlled`, meaning that `controlled` is only
* entered if the value of this condition is `v`.
*
* For details on what "controls" mean, see the QLDoc for `controls`.
*/
cached
predicate valueControls(IRBlock controlled, AbstractValue v) {
// This condition must determine the flow of control; that is, this
// node must be a top-level condition.
this.controlsBlock(controlled, v)
or
exists(IRGuardCondition ne |
this = ne.(LogicalNotInstruction).getUnary() and
ne.valueControls(controlled, v.getDualValue())
)
}

/**
* Holds if this condition controls `controlled`, meaning that `controlled` is only
Expand Down Expand Up @@ -282,13 +349,25 @@ class IRGuardCondition extends Instruction {
*/
cached
predicate controls(IRBlock controlled, boolean testIsTrue) {
// This condition must determine the flow of control; that is, this
// node must be a top-level condition.
this.controlsBlock(controlled, testIsTrue)
this.valueControls(controlled, any(BooleanValue bv | bv.getValue() = testIsTrue))
}

/**
* Holds if the control-flow edge `(pred, succ)` may be taken only if
* the value of this condition is `v`.
*/
cached
predicate valueControlsEdge(IRBlock pred, IRBlock succ, AbstractValue v) {
pred.getASuccessor() = succ and
this.valueControls(pred, v)
or
exists(IRGuardCondition ne |
this = ne.(LogicalNotInstruction).getUnary() and
ne.controls(controlled, testIsTrue.booleanNot())
succ = this.getBranchSuccessor(v) and
(
branch.(ConditionalBranchInstruction).getCondition() = this and
branch.getBlock() = pred
or
branch.(SwitchInstruction).getExpression() = this and
branch.getBlock() = pred
)
}

Expand All @@ -297,17 +376,12 @@ class IRGuardCondition extends Instruction {
* the value of this condition is `testIsTrue`.
*/
cached
predicate controlsEdge(IRBlock pred, IRBlock succ, boolean testIsTrue) {
pred.getASuccessor() = succ and
this.controls(pred, testIsTrue)
or
succ = this.getBranchSuccessor(testIsTrue) and
branch.getCondition() = this and
branch.getBlock() = pred
final predicate controlsEdge(IRBlock pred, IRBlock succ, boolean testIsTrue) {
this.valueControlsEdge(pred, succ, any(BooleanValue bv | bv.getValue() = testIsTrue))
}

/**
* Gets the block to which `branch` jumps directly when this condition is `testIsTrue`.
* Gets the block to which `branch` jumps directly when the value of this condition is `v`.
*
* This predicate is intended to help with situations in which an inference can only be made
* based on an edge between a block with multiple successors and a block with multiple
Expand All @@ -321,14 +395,20 @@ class IRGuardCondition extends Instruction {
* return x;
* ```
*/
private IRBlock getBranchSuccessor(boolean testIsTrue) {
branch.getCondition() = this and
(
testIsTrue = true and
result.getFirstInstruction() = branch.getTrueSuccessor()
private IRBlock getBranchSuccessor(AbstractValue v) {
branch.(ConditionalBranchInstruction).getCondition() = this and
exists(BooleanValue bv | bv = v |
bv.getValue() = true and
result.getFirstInstruction() = branch.(ConditionalBranchInstruction).getTrueSuccessor()
or
testIsTrue = false and
result.getFirstInstruction() = branch.getFalseSuccessor()
bv.getValue() = false and
result.getFirstInstruction() = branch.(ConditionalBranchInstruction).getFalseSuccessor()
)
or
exists(SwitchInstruction switch, CaseEdge kind | switch = branch |
switch.getExpression() = this and
result.getFirstInstruction() = switch.getSuccessor(kind) and
kind = v.(MatchValue).getCase()
)
}

Expand Down Expand Up @@ -396,11 +476,11 @@ class IRGuardCondition extends Instruction {

/**
* Holds if this condition controls `block`, meaning that `block` is only
* entered if the value of this condition is `testIsTrue`. This helper
* entered if the value of this condition is `v`. This helper
* predicate does not necessarily hold for binary logical operations like
* `&&` and `||`. See the detailed explanation on predicate `controls`.
*/
private predicate controlsBlock(IRBlock controlled, boolean testIsTrue) {
private predicate controlsBlock(IRBlock controlled, AbstractValue v) {
not isUnreachedBlock(controlled) and
//
// For this block to control the block `controlled` with `testIsTrue` the
Expand Down Expand Up @@ -441,7 +521,7 @@ class IRGuardCondition extends Instruction {
// that `this` strictly dominates `controlled` so that isn't necessary to check
// directly.
exists(IRBlock succ |
succ = this.getBranchSuccessor(testIsTrue) and
succ = this.getBranchSuccessor(v) and
this.hasDominatingEdgeTo(succ) and
succ.dominates(controlled)
)
Expand Down Expand Up @@ -476,12 +556,14 @@ class IRGuardCondition extends Instruction {
private IRBlock getBranchBlock() { result = branch.getBlock() }
}

private ConditionalBranchInstruction get_branch_for_condition(Instruction guard) {
result.getCondition() = guard
private Instruction getBranchForCondition(Instruction guard) {
result.(ConditionalBranchInstruction).getCondition() = guard
or
exists(LogicalNotInstruction cond |
result = get_branch_for_condition(cond) and cond.getUnary() = guard
result = getBranchForCondition(cond) and cond.getUnary() = guard
)
or
result.(SwitchInstruction).getExpression() = guard
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,6 @@
| test.cpp:18:8:18:10 | call to get |
| test.cpp:31:7:31:13 | ... == ... |
| test.cpp:42:13:42:20 | call to getABool |
| test.cpp:61:10:61:10 | i |
| test.cpp:74:10:74:10 | i |
| test.cpp:84:10:84:10 | i |
Original file line number Diff line number Diff line change
Expand Up @@ -86,3 +86,7 @@
| test.cpp:31:7:31:13 | ... == ... | true | 31 | 32 |
| test.cpp:42:13:42:20 | call to getABool | false | 53 | 53 |
| test.cpp:42:13:42:20 | call to getABool | true | 43 | 45 |
| test.cpp:61:10:61:10 | i | Case[0] | 62 | 64 |
| test.cpp:61:10:61:10 | i | Case[1] | 65 | 66 |
| test.cpp:74:10:74:10 | i | Case[0..10] | 75 | 77 |
| test.cpp:74:10:74:10 | i | Case[11..20] | 78 | 79 |
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@
import cpp
import semmle.code.cpp.controlflow.Guards

from GuardCondition guard, boolean sense, int start, int end
from GuardCondition guard, AbstractValue value, int start, int end
where
exists(BasicBlock block |
guard.controls(block, sense) and
guard.valueControls(block, value) and
block.hasLocationInfo(_, start, _, end, _)
)
select guard, sense, start, end
select guard, value, start, end
34 changes: 34 additions & 0 deletions cpp/ql/test/library-tests/controlflow/guards/test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,37 @@ bool testWithCatch0(int v)

return false;
}

void use1(int);
void use2(int);
void use3(int);

void test_switches_simple(int i) {
switch(i) {
case 0:
use1(i);
break;
case 1:
use2(i);
/* NOTE: fallthrough */
case 2:
use3(i);
}
}

void test_switches_range(int i) {
switch(i) {
case 0 ... 10:
use1(i);
break;
case 11 ... 20:
use2(i);
}
}

void test_switches_default(int i) {
switch(i) {
default:
use1(i);
}
}