Skip to content

Commit 799c380

Browse files
authored
Merge pull request #16255 from MathiasVP/fix-more-fps-in-iterator-to-expired-container
Fix more FPs in `cpp/iterator-to-expired-container`
2 parents bea7b94 + 77a7e00 commit 799c380

File tree

2 files changed

+77
-41
lines changed

2 files changed

+77
-41
lines changed

cpp/ql/src/experimental/Security/CWE/CWE-416/IteratorToExpiredContainer.ql

Lines changed: 55 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -23,29 +23,15 @@ private predicate tempToDestructorSink(DataFlow::Node sink, CallInstruction call
2323
call.getStaticCallTarget() instanceof Destructor
2424
}
2525

26-
/**
27-
* A configuration to track flow from a temporary variable to the qualifier of
28-
* a destructor call
29-
*/
30-
module TempToDestructorConfig implements DataFlow::ConfigSig {
31-
predicate isSource(DataFlow::Node source) {
32-
source.asInstruction().(VariableAddressInstruction).getIRVariable() instanceof IRTempVariable
33-
}
34-
35-
predicate isSink(DataFlow::Node sink) { tempToDestructorSink(sink, _) }
36-
}
37-
38-
module TempToDestructorFlow = DataFlow::Global<TempToDestructorConfig>;
39-
4026
/** Holds if `pun` is the post-update node of the qualifier of `Call`. */
4127
private predicate isPostUpdateOfQualifier(CallInstruction call, DataFlow::PostUpdateNode pun) {
4228
call.getThisArgumentOperand() = pun.getPreUpdateNode().asOperand()
4329
}
4430

4531
/**
4632
* Gets a `DataFlow::Node` that represents a temporary that will be destroyed
47-
* by a call to a destructor, or a `DataFlow::Node` that will transitively be
48-
* destroyed by a call to a destructor.
33+
* by a call to a destructor when `n` is destroyed, or a `DataFlow::Node` that
34+
* will transitively be destroyed by a call to a destructor.
4935
*
5036
* For the latter case, consider something like:
5137
* ```
@@ -57,23 +43,21 @@ private predicate isPostUpdateOfQualifier(CallInstruction call, DataFlow::PostUp
5743
* destroyed by a call to `std::vector<std::vector<int>>::~vector`,
5844
* and thus the result of `get_2d_vector()[0]` is also an invalid reference.
5945
*/
60-
DataFlow::Node getADestroyedNode() {
61-
exists(DataFlow::Node n | TempToDestructorFlow::flowTo(n) |
62-
// Case 1: The pointer that goes into the destructor call is destroyed
63-
exists(CallInstruction destructorCall |
64-
tempToDestructorSink(n, destructorCall) and
65-
isPostUpdateOfQualifier(destructorCall, result)
66-
)
67-
or
68-
// Case 2: Anything that was derived from the temporary that is now destroyed
69-
// is also destroyed.
70-
exists(CallInstruction call |
71-
result.asInstruction() = call and
72-
DataFlow::localFlow(DataFlow::operandNode(call.getThisArgumentOperand()), n)
73-
|
74-
call.getStaticCallTarget() instanceof StdSequenceContainerAt or
75-
call.getStaticCallTarget() instanceof StdMapAt
76-
)
46+
DataFlow::Node getADestroyedNode(DataFlow::Node n) {
47+
// Case 1: The pointer that goes into the destructor call is destroyed
48+
exists(CallInstruction destructorCall |
49+
tempToDestructorSink(n, destructorCall) and
50+
isPostUpdateOfQualifier(destructorCall, result)
51+
)
52+
or
53+
// Case 2: Anything that was derived from the temporary that is now destroyed
54+
// is also destroyed.
55+
exists(CallInstruction call |
56+
result.asInstruction() = call and
57+
DataFlow::localFlow(DataFlow::operandNode(call.getThisArgumentOperand()), n)
58+
|
59+
call.getStaticCallTarget() instanceof StdSequenceContainerAt or
60+
call.getStaticCallTarget() instanceof StdMapAt
7761
)
7862
}
7963

@@ -86,12 +70,39 @@ predicate destroyedToBeginSink(DataFlow::Node sink, FunctionCall fc) {
8670
}
8771

8872
/**
89-
* Flow from any destroyed object to the qualifier of a `begin` or `end` call
73+
* A configuration to track flow from a temporary variable to the qualifier of
74+
* a destructor call, and subsequently to a qualifier of a call to `begin` or
75+
* `end`.
9076
*/
91-
module DestroyedToBeginConfig implements DataFlow::ConfigSig {
92-
predicate isSource(DataFlow::Node source) { source = getADestroyedNode() }
77+
module Config implements DataFlow::StateConfigSig {
78+
newtype FlowState =
79+
additional TempToDestructor() or
80+
additional DestroyedToBegin(DataFlow::Node n) {
81+
exists(DataFlow::Node thisOperand |
82+
tempToDestructorSink(thisOperand, _) and
83+
n = getADestroyedNode(thisOperand)
84+
)
85+
}
86+
87+
predicate isSource(DataFlow::Node source, FlowState state) {
88+
source.asInstruction().(VariableAddressInstruction).getIRVariable() instanceof IRTempVariable and
89+
state = TempToDestructor()
90+
}
91+
92+
predicate isAdditionalFlowStep(
93+
DataFlow::Node node1, FlowState state1, DataFlow::Node node2, FlowState state2
94+
) {
95+
tempToDestructorSink(node1, _) and
96+
state1 = TempToDestructor() and
97+
state2 = DestroyedToBegin(node2) and
98+
node2 = getADestroyedNode(node1)
99+
}
93100

94-
predicate isSink(DataFlow::Node sink) { destroyedToBeginSink(sink, _) }
101+
predicate isSink(DataFlow::Node sink, FlowState state) {
102+
// Note: This is a non-trivial cartesian product!
103+
// Hopefully, both of these sets are quite small in practice
104+
destroyedToBeginSink(sink, _) and state instanceof DestroyedToBegin
105+
}
95106

96107
DataFlow::FlowFeature getAFeature() {
97108
// By blocking argument-to-parameter flow we ensure that we don't enter a
@@ -108,8 +119,11 @@ module DestroyedToBeginConfig implements DataFlow::ConfigSig {
108119
}
109120
}
110121

111-
module DestroyedToBeginFlow = DataFlow::Global<DestroyedToBeginConfig>;
122+
module Flow = DataFlow::GlobalWithState<Config>;
112123

113-
from DataFlow::Node source, DataFlow::Node sink, FunctionCall beginOrEnd
114-
where DestroyedToBeginFlow::flow(source, sink) and destroyedToBeginSink(sink, beginOrEnd)
115-
select source, "This object is destroyed before $@ is called.", beginOrEnd, beginOrEnd.toString()
124+
from Flow::PathNode source, Flow::PathNode sink, FunctionCall beginOrEnd, DataFlow::Node mid
125+
where
126+
Flow::flowPath(source, sink) and
127+
destroyedToBeginSink(sink.getNode(), beginOrEnd) and
128+
sink.getState() = Config::DestroyedToBegin(mid)
129+
select mid, "This object is destroyed before $@ is called.", beginOrEnd, beginOrEnd.toString()

cpp/ql/test/experimental/query-tests/Security/CWE/CWE-416/test.cpp

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -770,4 +770,26 @@ void test2() {
770770
void test3() {
771771
const std::vector<std::vector<int>>& v = returnValue(); // GOOD
772772
for(const std::vector<int>& x : v) {}
773+
}
774+
775+
struct A : public std::vector<int> {
776+
void foo(std::vector<int>& result) {
777+
int i = 0;
778+
while (i < 10) {
779+
A chunk;
780+
result.insert(result.end(), chunk.begin(), chunk.end());
781+
++i;
782+
}
783+
}
784+
785+
~A() = default;
786+
};
787+
788+
void test4() {
789+
// This creates a temporary, after which `~A` is called at the semicolon, and
790+
// `~A` calls `~vector<int>` inside the compiler-generated destructor.
791+
// If we don't preserve the call context and return to the destructor call in this
792+
// function we may end up in the destructor call `chunk.~A()`in `A.foo`. This destructor
793+
// call can flow to `begin` through the back-edge and cause a strange FP.
794+
auto zero = A().size();
773795
}

0 commit comments

Comments
 (0)