Skip to content

Commit 5f01419

Browse files
authored
Merge pull request github#4644 from MathiasVP/unsafe-use-of-this-query
C++: Add 'unsafe use of this' query
2 parents 48460e3 + 648acc3 commit 5f01419

File tree

8 files changed

+377
-0
lines changed

8 files changed

+377
-0
lines changed
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
lgtm,codescanning
2+
* A new query (`cpp/unsafe-use-of-this`) has been added. The query finds pure virtual function calls whose qualifier is an object under construction.

cpp/config/suites/cpp/correctness

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
+ semmlecode-cpp-queries/Likely Bugs/Conversion/CastArrayPointerArithmetic.ql: /Correctness/Dangerous Conversions
1010
+ semmlecode-cpp-queries/Likely Bugs/Underspecified Functions/MistypedFunctionArguments.ql: /Correctness/Dangerous Conversions
1111
+ semmlecode-cpp-queries/Security/CWE/CWE-253/HResultBooleanConversion.ql: /Correctness/Dangerous Conversions
12+
+ semmlecode-cpp-queries/Likely Bugs/OO/UnsafeUseOfThis.ql: /Correctness/Dangerous Conversions
1213
# Consistent Use
1314
+ semmlecode-cpp-queries/Critical/ReturnValueIgnored.ql: /Correctness/Consistent Use
1415
+ semmlecode-cpp-queries/Likely Bugs/InconsistentCheckReturnNull.ql: /Correctness/Consistent Use
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
class Base {
2+
private:
3+
// pure virtual member function used for initialization of derived classes.
4+
virtual void construct() = 0;
5+
public:
6+
Base() {
7+
// wrong: the virtual table of `Derived` has not been initialized yet. So this
8+
// call will resolve to `Base::construct`, which cannot be called as it is a pure
9+
// virtual function.
10+
construct();
11+
}
12+
};
13+
14+
class Derived : public Base {
15+
int field;
16+
17+
void construct() override {
18+
field = 1;
19+
}
20+
};
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<!DOCTYPE qhelp PUBLIC
2+
"-//Semmle//qhelp//EN"
3+
"qhelp.dtd">
4+
<qhelp>
5+
6+
7+
<overview>
8+
<p>This rule finds calls to pure virtual member functions in constructors and destructors. When executing the body of a constructor of class <code>T</code>, the virtual table of <code>T</code> refers to the virtual table of one of <code>T</code>'s base classes. This can produce unexpected behavior, including program abort that can lead to denial of service attacks. The same problem exists during destruction of an object.</p>
9+
10+
</overview>
11+
<recommendation>
12+
<p>Do not rely on virtual dispatch in constructors and destructors. Instead, each class should be responsible for acquiring and releasing its resources. If a base class needs to refer to a derived class during initialization, use the Dynamic Binding During Initialization idiom.</p>
13+
14+
</recommendation>
15+
<example><sample src="UnsafeUseOfThis.cpp" />
16+
17+
18+
19+
</example>
20+
<references>
21+
22+
<li>ISO C++ FAQ: <a href="https://isocpp.org/wiki/faq/strange-inheritance#calling-virtuals-from-ctors">When my base class's constructor calls a virtual function on its this object, why doesn't my derived class's override of that virtual function get invoked?</a>
23+
</li>
24+
<li>SEI CERT C++ Coding Standard <a href="https://wiki.sei.cmu.edu/confluence/display/cplusplus/OOP50-CPP.+Do+not+invoke+virtual+functions+from+constructors+or+destructors">OOP50-CPP. Do not invoke virtual functions from constructors or destructors</a>
25+
</li>
26+
<li>ISO C++ FAQ: <a href="https://isocpp.org/wiki/faq/strange-inheritance#calling-virtuals-from-ctor-idiom">Okay, but is there a way to simulate that behavior as if dynamic binding worked on the this object within my base class's constructor?</a>
27+
</li>
28+
29+
30+
</references></qhelp>
Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
/**
2+
* @name Unsafe use of this in constructor
3+
* @description A call to a pure virtual function using a 'this'
4+
* pointer of an object that is under construction
5+
* may lead to undefined behavior.
6+
* @kind path-problem
7+
* @id cpp/unsafe-use-of-this
8+
* @problem.severity error
9+
* @precision very-high
10+
* @tags correctness
11+
* language-features
12+
* security
13+
*/
14+
15+
import cpp
16+
// We don't actually use the global value numbering library in this query, but without it we end up
17+
// recomputing the IR.
18+
private import semmle.code.cpp.valuenumbering.GlobalValueNumbering
19+
private import semmle.code.cpp.ir.IR
20+
21+
bindingset[n, result]
22+
int unbind(int n) { result >= n and result <= n }
23+
24+
/** Holds if `p` is the `n`'th parameter of the non-virtual function `f`. */
25+
predicate parameterOf(Parameter p, Function f, int n) {
26+
not f.isVirtual() and f.getParameter(n) = p
27+
}
28+
29+
/**
30+
* Holds if `instr` is the `n`'th argument to a call to the non-virtual function `f`, and
31+
* `init` is the corresponding initiazation instruction that receives the value of `instr` in `f`.
32+
*/
33+
predicate flowIntoParameter(
34+
CallInstruction call, Instruction instr, Function f, int n, InitializeParameterInstruction init
35+
) {
36+
not f.isVirtual() and
37+
call.getPositionalArgument(n) = instr and
38+
f = call.getStaticCallTarget() and
39+
getEnclosingNonVirtualFunctionInitializeParameter(init, f) and
40+
init.getParameter().getIndex() = unbind(n)
41+
}
42+
43+
/**
44+
* Holds if `instr` is an argument to a call to the function `f`, and `init` is the
45+
* corresponding initialization instruction that receives the value of `instr` in `f`.
46+
*/
47+
pragma[noinline]
48+
predicate getPositionalArgumentInitParam(
49+
CallInstruction call, Instruction instr, InitializeParameterInstruction init, Function f
50+
) {
51+
exists(int n |
52+
parameterOf(_, f, n) and
53+
flowIntoParameter(call, instr, f, unbind(n), init)
54+
)
55+
}
56+
57+
/**
58+
* Holds if `instr` is the qualifier to a call to the non-virtual function `f`, and
59+
* `init` is the corresponding initiazation instruction that receives the value of
60+
* `instr` in `f`.
61+
*/
62+
pragma[noinline]
63+
predicate getThisArgumentInitParam(
64+
CallInstruction call, Instruction instr, InitializeParameterInstruction init, Function f
65+
) {
66+
not f.isVirtual() and
67+
call.getStaticCallTarget() = f and
68+
getEnclosingNonVirtualFunctionInitializeParameter(init, f) and
69+
call.getThisArgument() = instr and
70+
init.getIRVariable() instanceof IRThisVariable
71+
}
72+
73+
/** Holds if `instr` is a `this` pointer used by the call instruction `call`. */
74+
predicate isSink(Instruction instr, CallInstruction call) {
75+
exists(PureVirtualFunction func |
76+
call.getStaticCallTarget() = func and
77+
call.getThisArgument() = instr and
78+
// Weed out implicit calls to destructors of a base class
79+
not func instanceof Destructor
80+
)
81+
}
82+
83+
/** Holds if `init` initializes the `this` pointer in class `c`. */
84+
predicate isSource(InitializeParameterInstruction init, string msg, Class c) {
85+
(
86+
exists(Constructor func |
87+
not func instanceof CopyConstructor and
88+
not func instanceof MoveConstructor and
89+
func = init.getEnclosingFunction() and
90+
msg = "construction"
91+
)
92+
or
93+
init.getEnclosingFunction() instanceof Destructor and msg = "destruction"
94+
) and
95+
init.getIRVariable() instanceof IRThisVariable and
96+
init.getEnclosingFunction().getDeclaringType() = c
97+
}
98+
99+
/**
100+
* Holds if `instr` flows to a sink (which is a use of the value of `instr` as a `this` pointer).
101+
*/
102+
predicate flowsToSink(Instruction instr, Instruction sink) {
103+
flowsFromSource(instr) and
104+
(
105+
isSink(instr, _) and instr = sink
106+
or
107+
exists(Instruction mid |
108+
successor(instr, mid) and
109+
flowsToSink(mid, sink)
110+
)
111+
)
112+
}
113+
114+
/** Holds if `instr` flows from a source. */
115+
predicate flowsFromSource(Instruction instr) {
116+
isSource(instr, _, _)
117+
or
118+
exists(Instruction mid |
119+
successor(mid, instr) and
120+
flowsFromSource(mid)
121+
)
122+
}
123+
124+
/** Holds if `f` is the enclosing non-virtual function of `init`. */
125+
predicate getEnclosingNonVirtualFunctionInitializeParameter(
126+
InitializeParameterInstruction init, Function f
127+
) {
128+
not f.isVirtual() and
129+
init.getEnclosingFunction() = f
130+
}
131+
132+
/** Holds if `f` is the enclosing non-virtual function of `init`. */
133+
predicate getEnclosingNonVirtualFunctionInitializeIndirection(
134+
InitializeIndirectionInstruction init, Function f
135+
) {
136+
not f.isVirtual() and
137+
init.getEnclosingFunction() = f
138+
}
139+
140+
/**
141+
* Holds if `instr` is an argument (or argument indirection) to a call, and
142+
* `succ` is the corresponding initialization instruction in the call target.
143+
*/
144+
predicate flowThroughCallable(Instruction instr, Instruction succ) {
145+
// Flow from an argument to a parameter
146+
exists(CallInstruction call, InitializeParameterInstruction init | init = succ |
147+
getPositionalArgumentInitParam(call, instr, init, call.getStaticCallTarget())
148+
or
149+
getThisArgumentInitParam(call, instr, init, call.getStaticCallTarget())
150+
)
151+
or
152+
// Flow from argument indirection to parameter indirection
153+
exists(
154+
CallInstruction call, ReadSideEffectInstruction read, InitializeIndirectionInstruction init
155+
|
156+
init = succ and
157+
read.getPrimaryInstruction() = call and
158+
getEnclosingNonVirtualFunctionInitializeIndirection(init, call.getStaticCallTarget())
159+
|
160+
exists(int n |
161+
read.getSideEffectOperand().getAnyDef() = instr and
162+
read.getIndex() = n and
163+
init.getParameter().getIndex() = unbind(n)
164+
)
165+
or
166+
call.getThisArgument() = instr and
167+
init.getIRVariable() instanceof IRThisVariable
168+
)
169+
}
170+
171+
/** Holds if `instr` flows to `succ`. */
172+
predicate successor(Instruction instr, Instruction succ) {
173+
succ.(CopyInstruction).getSourceValue() = instr or
174+
succ.(CheckedConvertOrNullInstruction).getUnary() = instr or
175+
succ.(ChiInstruction).getTotal() = instr or
176+
succ.(ConvertInstruction).getUnary() = instr or
177+
succ.(InheritanceConversionInstruction).getUnary() = instr or
178+
flowThroughCallable(instr, succ)
179+
}
180+
181+
/**
182+
* Holds if:
183+
* - `source` is an initialization of a `this` pointer of type `sourceClass`, and
184+
* - `sink` is a use of the `this` pointer, and
185+
* - `call` invokes a pure virtual function using `sink` as the `this` pointer, and
186+
* - `msg` is a string describing whether `source` is from a constructor or destructor.
187+
*/
188+
predicate flows(
189+
Instruction source, string msg, Class sourceClass, Instruction sink, CallInstruction call
190+
) {
191+
isSource(source, msg, sourceClass) and
192+
flowsToSink(source, sink) and
193+
isSink(sink, call)
194+
}
195+
196+
query predicate edges(Instruction a, Instruction b) { successor(a, b) and flowsToSink(b, _) }
197+
198+
query predicate nodes(Instruction n, string key, string val) {
199+
flowsToSink(n, _) and
200+
key = "semmle.label" and
201+
val = n.toString()
202+
}
203+
204+
from Instruction source, Instruction sink, CallInstruction call, string msg, Class sourceClass
205+
where
206+
flows(source, msg, sourceClass, sink, call) and
207+
// Only raise an alert if there is no override of the pure virtual function in any base class.
208+
not exists(Class c | c = sourceClass.getABaseClass*() |
209+
c.getAMemberFunction().getAnOverriddenFunction() = call.getStaticCallTarget()
210+
)
211+
select call.getUnconvertedResultExpression(), source, sink,
212+
"Call to pure virtual function during " + msg
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
edges
2+
| test.cpp:7:3:7:3 | InitializeParameter: B | test.cpp:8:12:8:15 | Load: this |
3+
| test.cpp:8:12:8:15 | Load: this | test.cpp:34:16:34:16 | InitializeParameter: x |
4+
| test.cpp:11:8:11:8 | InitializeParameter: b | test.cpp:12:5:12:5 | Load: b |
5+
| test.cpp:12:5:12:5 | CopyValue: (reference dereference) | test.cpp:12:5:12:5 | ConvertToNonVirtualBase: (A)... |
6+
| test.cpp:12:5:12:5 | Load: b | test.cpp:12:5:12:5 | CopyValue: (reference dereference) |
7+
| test.cpp:15:3:15:4 | InitializeParameter: ~B | test.cpp:16:5:16:5 | Load: this |
8+
| test.cpp:16:5:16:5 | Load: this | file://:0:0:0:0 | ConvertToNonVirtualBase: (A *)... |
9+
| test.cpp:21:3:21:3 | InitializeParameter: C | test.cpp:21:13:21:13 | ConvertToNonVirtualBase: call to B |
10+
| test.cpp:21:3:21:3 | InitializeParameter: C | test.cpp:22:12:22:15 | Load: this |
11+
| test.cpp:21:3:21:3 | InitializeParameter: C | test.cpp:25:7:25:10 | Load: this |
12+
| test.cpp:21:13:21:13 | ConvertToNonVirtualBase: call to B | test.cpp:7:3:7:3 | InitializeParameter: B |
13+
| test.cpp:22:12:22:15 | ConvertToNonVirtualBase: (B *)... | test.cpp:34:16:34:16 | InitializeParameter: x |
14+
| test.cpp:22:12:22:15 | Load: this | test.cpp:22:12:22:15 | ConvertToNonVirtualBase: (B *)... |
15+
| test.cpp:25:7:25:10 | ConvertToNonVirtualBase: (B *)... | test.cpp:25:7:25:10 | ConvertToNonVirtualBase: (A *)... |
16+
| test.cpp:25:7:25:10 | Load: this | test.cpp:25:7:25:10 | ConvertToNonVirtualBase: (B *)... |
17+
| test.cpp:31:3:31:3 | InitializeParameter: D | test.cpp:31:12:31:15 | Load: this |
18+
| test.cpp:31:11:31:15 | ConvertToNonVirtualBase: (B)... | test.cpp:31:11:31:15 | CopyValue: (reference to) |
19+
| test.cpp:31:11:31:15 | CopyValue: (reference to) | test.cpp:11:8:11:8 | InitializeParameter: b |
20+
| test.cpp:31:11:31:15 | CopyValue: * ... | test.cpp:31:11:31:15 | ConvertToNonVirtualBase: (B)... |
21+
| test.cpp:31:12:31:15 | Load: this | test.cpp:31:11:31:15 | CopyValue: * ... |
22+
| test.cpp:34:16:34:16 | InitializeParameter: x | test.cpp:35:3:35:3 | Load: x |
23+
| test.cpp:35:3:35:3 | Load: x | test.cpp:35:3:35:3 | ConvertToNonVirtualBase: (A *)... |
24+
| test.cpp:47:3:47:3 | InitializeParameter: F | test.cpp:48:10:48:13 | Load: this |
25+
| test.cpp:48:10:48:13 | ConvertToNonVirtualBase: (E *)... | test.cpp:48:6:48:13 | ConvertToNonVirtualBase: (A *)... |
26+
| test.cpp:48:10:48:13 | Load: this | test.cpp:48:10:48:13 | ConvertToNonVirtualBase: (E *)... |
27+
nodes
28+
| file://:0:0:0:0 | ConvertToNonVirtualBase: (A *)... | semmle.label | ConvertToNonVirtualBase: (A *)... |
29+
| test.cpp:7:3:7:3 | InitializeParameter: B | semmle.label | InitializeParameter: B |
30+
| test.cpp:8:12:8:15 | Load: this | semmle.label | Load: this |
31+
| test.cpp:11:8:11:8 | InitializeParameter: b | semmle.label | InitializeParameter: b |
32+
| test.cpp:12:5:12:5 | ConvertToNonVirtualBase: (A)... | semmle.label | ConvertToNonVirtualBase: (A)... |
33+
| test.cpp:12:5:12:5 | CopyValue: (reference dereference) | semmle.label | CopyValue: (reference dereference) |
34+
| test.cpp:12:5:12:5 | Load: b | semmle.label | Load: b |
35+
| test.cpp:15:3:15:4 | InitializeParameter: ~B | semmle.label | InitializeParameter: ~B |
36+
| test.cpp:16:5:16:5 | Load: this | semmle.label | Load: this |
37+
| test.cpp:21:3:21:3 | InitializeParameter: C | semmle.label | InitializeParameter: C |
38+
| test.cpp:21:13:21:13 | ConvertToNonVirtualBase: call to B | semmle.label | ConvertToNonVirtualBase: call to B |
39+
| test.cpp:22:12:22:15 | ConvertToNonVirtualBase: (B *)... | semmle.label | ConvertToNonVirtualBase: (B *)... |
40+
| test.cpp:22:12:22:15 | Load: this | semmle.label | Load: this |
41+
| test.cpp:25:7:25:10 | ConvertToNonVirtualBase: (A *)... | semmle.label | ConvertToNonVirtualBase: (A *)... |
42+
| test.cpp:25:7:25:10 | ConvertToNonVirtualBase: (B *)... | semmle.label | ConvertToNonVirtualBase: (B *)... |
43+
| test.cpp:25:7:25:10 | Load: this | semmle.label | Load: this |
44+
| test.cpp:31:3:31:3 | InitializeParameter: D | semmle.label | InitializeParameter: D |
45+
| test.cpp:31:11:31:15 | ConvertToNonVirtualBase: (B)... | semmle.label | ConvertToNonVirtualBase: (B)... |
46+
| test.cpp:31:11:31:15 | CopyValue: (reference to) | semmle.label | CopyValue: (reference to) |
47+
| test.cpp:31:11:31:15 | CopyValue: * ... | semmle.label | CopyValue: * ... |
48+
| test.cpp:31:12:31:15 | Load: this | semmle.label | Load: this |
49+
| test.cpp:34:16:34:16 | InitializeParameter: x | semmle.label | InitializeParameter: x |
50+
| test.cpp:35:3:35:3 | ConvertToNonVirtualBase: (A *)... | semmle.label | ConvertToNonVirtualBase: (A *)... |
51+
| test.cpp:35:3:35:3 | Load: x | semmle.label | Load: x |
52+
| test.cpp:47:3:47:3 | InitializeParameter: F | semmle.label | InitializeParameter: F |
53+
| test.cpp:48:6:48:13 | ConvertToNonVirtualBase: (A *)... | semmle.label | ConvertToNonVirtualBase: (A *)... |
54+
| test.cpp:48:10:48:13 | ConvertToNonVirtualBase: (E *)... | semmle.label | ConvertToNonVirtualBase: (E *)... |
55+
| test.cpp:48:10:48:13 | Load: this | semmle.label | Load: this |
56+
#select
57+
| test.cpp:12:7:12:7 | call to f | test.cpp:31:3:31:3 | InitializeParameter: D | test.cpp:12:5:12:5 | ConvertToNonVirtualBase: (A)... | Call to pure virtual function during construction |
58+
| test.cpp:16:5:16:5 | call to f | test.cpp:15:3:15:4 | InitializeParameter: ~B | file://:0:0:0:0 | ConvertToNonVirtualBase: (A *)... | Call to pure virtual function during destruction |
59+
| test.cpp:25:13:25:13 | call to f | test.cpp:21:3:21:3 | InitializeParameter: C | test.cpp:25:7:25:10 | ConvertToNonVirtualBase: (A *)... | Call to pure virtual function during construction |
60+
| test.cpp:35:6:35:6 | call to f | test.cpp:7:3:7:3 | InitializeParameter: B | test.cpp:35:3:35:3 | ConvertToNonVirtualBase: (A *)... | Call to pure virtual function during construction |
61+
| test.cpp:35:6:35:6 | call to f | test.cpp:21:3:21:3 | InitializeParameter: C | test.cpp:35:3:35:3 | ConvertToNonVirtualBase: (A *)... | Call to pure virtual function during construction |
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Likely Bugs/OO/UnsafeUseOfThis.ql
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
struct A { virtual void f() = 0; };
2+
3+
struct B;
4+
void call_f(B*);
5+
6+
struct B : public A {
7+
B() {
8+
call_f(this);
9+
}
10+
11+
B(B& b) {
12+
b.f(); // BAD: undefined behavior
13+
}
14+
15+
~B() {
16+
f(); // BAD: undefined behavior
17+
}
18+
};
19+
20+
struct C : public B {
21+
C(bool b) {
22+
call_f(this);
23+
24+
if(b) {
25+
this->f(); // BAD: undefined behavior
26+
}
27+
}
28+
};
29+
30+
struct D : public B {
31+
D() : B(*this) {}
32+
};
33+
34+
void call_f(B* x) {
35+
x->f(); // 2 x BAD: Undefined behavior
36+
}
37+
38+
struct E : public A {
39+
E() {
40+
f(); // GOOD: Will call `E::f`
41+
}
42+
43+
void f() override {}
44+
};
45+
46+
struct F : public E {
47+
F() {
48+
((A*)this)->f(); // GOOD: Will call `E::f`
49+
}
50+
};

0 commit comments

Comments
 (0)