Skip to content
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

C++: Generate IR for destruction of unconditionally constructed temporaries #16125

Merged
merged 39 commits into from
Apr 12, 2024
Merged
Show file tree
Hide file tree
Changes from 35 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
75c453f
C++: Unsuppress temporary destructors in IR
rdmarsh2 Mar 5, 2024
894d934
C++: Accept test changes.
MathiasVP Apr 4, 2024
17e8c95
C++: suppress destructors on conditional temporaries
rdmarsh2 Mar 12, 2024
cf996f8
C++: Accept test changes.
MathiasVP Apr 4, 2024
d4e2d37
C++: Add a simple test that fails.
MathiasVP Apr 4, 2024
a756f14
C++: Only report implicit destructors if we need to translate them.
MathiasVP Apr 4, 2024
56a132f
C++: Accept test changes.
MathiasVP Apr 4, 2024
796fcfe
C++: Handle conversions in 'isInConditionalEvaluation'.
MathiasVP Apr 4, 2024
a6a0e20
C++: Accept test changes.
MathiasVP Apr 4, 2024
73602dc
C++: Also suppress destructor calls on throwing ternary expressions.
MathiasVP Apr 4, 2024
0b7070f
C++: Accept test changes.
MathiasVP Apr 4, 2024
774efb5
Merge branch 'main' into destructors-for-unconditional-unnamed
MathiasVP Apr 4, 2024
805b4d6
C++: Add a failing testcase.
MathiasVP Apr 4, 2024
1808886
C++: Properly handle the case where a TranslatedElement has no children.
MathiasVP Apr 4, 2024
8f11cb6
C++: Accept test changes.
MathiasVP Apr 4, 2024
587ae07
C++: Accept query test changes.
MathiasVP Apr 4, 2024
f098b8e
C++: Make sure the edge kind out of a throw is an 'ExceptionEdge' eve…
MathiasVP Apr 4, 2024
b6ddb97
C++: Accept test changes.
MathiasVP Apr 4, 2024
e63a607
C++: Add another test with conditional construction.
MathiasVP Apr 5, 2024
d279e3f
C++: Suppress destructor calls for the right-hand side of logical ope…
MathiasVP Apr 5, 2024
bb2c690
C++: Accept test changes.
MathiasVP Apr 5, 2024
b042366
C++: Add a failing testcase.
MathiasVP Apr 5, 2024
f1d2dac
C++: Fix a bug where the destructor attached to a 'new' expression would
MathiasVP Apr 5, 2024
4c01c06
C++: Accept test changes.
MathiasVP Apr 5, 2024
955f9c7
C++: Add a failing testcase.
MathiasVP Apr 5, 2024
54e4103
C++: Fix another multiple parents problem.
MathiasVP Apr 5, 2024
45e7154
C++: Accept test changes.
MathiasVP Apr 5, 2024
a0de95d
C++: Add testcases that produces an 'missingOperandType' and 'missing…
MathiasVP Apr 6, 2024
89eaadd
C++: Move destructor calls from expressions with a temporary object c…
MathiasVP Apr 7, 2024
fcd0e99
C++: Accept test changes.
MathiasVP Apr 7, 2024
8a6a60e
C++: Also handle destructor calls on converted expressions in PrintAST.
MathiasVP Apr 7, 2024
d40fa4c
C++: Accept test changes.
MathiasVP Apr 7, 2024
febd060
C++: Add testcase where two destructor calls are remapped to a tempor…
MathiasVP Apr 8, 2024
9c25ce4
C++: Add testcase with two destructor calls without a temporary objec…
MathiasVP Apr 8, 2024
4fa53b6
Merge branch 'main' into destructors-for-unconditional-unnamed
MathiasVP Apr 8, 2024
17c8fa3
Update cpp/ql/lib/semmle/code/cpp/ir/implementation/raw/internal/Tran…
MathiasVP Apr 9, 2024
c325a79
C++: Ensure 'isConditionalTemporaryDestructorCall' only holds when th…
MathiasVP Apr 9, 2024
7172e2f
Merge branch 'main' into destructors-for-unconditional-unnamed
MathiasVP Apr 10, 2024
736d59c
Merge branch 'main' into destructors-for-unconditional-unnamed
MathiasVP Apr 11, 2024
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
2 changes: 2 additions & 0 deletions cpp/ql/lib/semmle/code/cpp/PrintAST.qll
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,8 @@ class ConversionNode extends ExprNode {
childIndex = 0 and
result.getAst() = conv.getExpr() and
conv.getExpr() instanceof Conversion
or
result.getAst() = expr.getImplicitDestructorCall(childIndex - 1)
}
}

Expand Down
6 changes: 6 additions & 0 deletions cpp/ql/lib/semmle/code/cpp/exprs/Expr.qll
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,12 @@ class Expr extends StmtParent, @expr {
* order of destruction.
*/
DestructorCall getImplicitDestructorCall(int n) {
exists(Expr e |
e = this.(TemporaryObjectExpr).getExpr() and
synthetic_destructor_call(e, max(int i | synthetic_destructor_call(e, i, _)) - n, result)
)
or
not this = any(TemporaryObjectExpr temp).getExpr() and
jketema marked this conversation as resolved.
Show resolved Hide resolved
synthetic_destructor_call(this, max(int i | synthetic_destructor_call(this, i, _)) - n, result)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,10 @@ private predicate ignoreExprAndDescendants(Expr expr) {
vaStartExpr.getLastNamedParameter().getFullyConverted() = expr
)
or
// suppress destructors of temporary variables until proper support is added for them.
exists(Expr parent | parent.getAnImplicitDestructorCall() = expr)
// Do not translate implicit destructor calls for unnamed temporary variables that are
// conditionally constructed (until we have a mechanism for calling these only when the
// temporary's constructor was run)
isConditionalTemporaryDestructorCall(expr)
}

/**
Expand Down Expand Up @@ -255,6 +257,39 @@ private predicate usedAsCondition(Expr expr) {
)
}

private predicate hasThrowingChild(Expr e) {
e = any(ThrowExpr throw).getFullyConverted()
or
exists(Expr child |
e = getRealParent(child) and
hasThrowingChild(child)
)
}

private predicate isInConditionalEvaluation(Expr e) {
exists(ConditionalExpr cond |
e = cond.getThen().getFullyConverted() and not cond.isTwoOperand()
or
e = cond.getElse().getFullyConverted()
or
// If one of the operands throws then the temporaries constructed in either
// branch will also be attached to the ternary expression. We suppress
// those destructor calls as well.
hasThrowingChild([cond.getThen(), cond.getElse()]) and
e = cond.getFullyConverted()
)
or
e = any(LogicalAndExpr lae).getRightOperand().getFullyConverted()
or
e = any(LogicalOrExpr loe).getRightOperand().getFullyConverted()
or
isInConditionalEvaluation(getRealParent(e))
}

private predicate isConditionalTemporaryDestructorCall(DestructorCall dc) {
isInConditionalEvaluation(dc.getQualifier().(ReuseExpr).getReusedExpr())
jketema marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* Holds if `conv` is an `InheritanceConversion` that requires a `TranslatedLoad`, despite not being
* marked as having an lvalue-to-rvalue conversion.
Expand Down Expand Up @@ -783,6 +818,16 @@ newtype TTranslatedElement =
not ignoreSideEffects(expr) and
opcode = getCallSideEffectOpcode(expr)
} or
// The set of destructors to invoke after a `throw`. These need to be special
// cased because the edge kind following a throw is an `ExceptionEdge`, and
// we need to make sure that the edge kind is still an `ExceptionEdge` after
// all the destructors has run.
MathiasVP marked this conversation as resolved.
Show resolved Hide resolved
TTranslatedDestructorsAfterThrow(ThrowExpr throw) {
exists(DestructorCall dc |
dc = throw.getAnImplicitDestructorCall() and
not ignoreExpr(dc)
)
} or
// A precise side effect of an argument to a `Call`
TTranslatedArgumentExprSideEffect(Call call, Expr expr, int n, SideEffectOpcode opcode) {
not ignoreExpr(expr) and
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,10 +90,26 @@ abstract class TranslatedExpr extends TranslatedElement {
final override TranslatedElement getChild(int id) {
result = this.getChildInternal(id)
or
exists(int maxChildId, int destructorIndex |
maxChildId = max(int childId | exists(this.getChildInternal(childId))) and
result.(TranslatedExpr).getExpr() = expr.getImplicitDestructorCall(destructorIndex) and
id = maxChildId + 1 + destructorIndex
exists(int destructorIndex |
result = this.getImplicitDestructorCall(destructorIndex) and
id = this.getFirstDestructorCallIndex() + destructorIndex
)
}

final private TranslatedExpr getImplicitDestructorCall(int index) {
result.getExpr() = expr.getImplicitDestructorCall(index)
}

final override predicate hasAnImplicitDestructorCall() {
exists(this.getImplicitDestructorCall(_))
}

final override int getFirstDestructorCallIndex() {
not this.handlesDestructorsExplicitly() and
(
result = max(int childId | exists(this.getChildInternal(childId))) + 1
or
not exists(this.getChildInternal(_)) and result = 0
)
}

Expand Down Expand Up @@ -395,6 +411,14 @@ class TranslatedLoad extends TranslatedValueCategoryAdjustment, TTranslatedLoad
result = this.getOperand().getResult()
)
}

override predicate handlesDestructorsExplicitly() {
// The class that generates IR for `e` will (implicitly or explicitly)
// handle the generation of destructor calls for `e`. Without disabling
// destructor call generation here the destructor will get multiple
// parents.
any()
}
}

/**
Expand Down Expand Up @@ -1999,6 +2023,13 @@ abstract class TranslatedAllocationSize extends TranslatedExpr, TTranslatedAlloc
final override predicate producesExprResult() { none() }

final override Instruction getResult() { result = this.getInstruction(AllocationSizeTag()) }

final override predicate handlesDestructorsExplicitly() {
// Since the enclosing `TranslatedNewOrNewArrayExpr` (implicitly) handles the destructors
// we need to disable the implicit handling here as otherwise the destructors will have
// multiple parents
any()
}
}

TranslatedAllocationSize getTranslatedAllocationSize(NewOrNewArrayExpr newExpr) {
Expand Down Expand Up @@ -2156,6 +2187,13 @@ class TranslatedAllocatorCall extends TTranslatedAllocatorCall, TranslatedDirect

final override predicate producesExprResult() { none() }

final override predicate handlesDestructorsExplicitly() {
// Since the enclosing `TranslatedNewOrNewArrayExpr` (implicitly) handles the destructors
// we need to disable the implicit handling here as otherwise the destructors will have
// multiple parents
any()
}

override Function getInstructionFunction(InstructionTag tag) {
tag = CallTargetTag() and result = expr.getAllocator()
}
Expand Down Expand Up @@ -2817,6 +2855,68 @@ class TranslatedReuseExpr extends TranslatedNonConstantExpr {
}
}

/**
* The IR translation of the destructor calls of the parent `TranslatedThrow`.
*
* This object does not itself generate the destructor calls. Instead, its
* children provide the actual calls, and this object ensures that we correctly
* exit with an `ExceptionEdge` after executing all the destructor calls.
*/
class TranslatedDestructorsAfterThrow extends TranslatedElement, TTranslatedDestructorsAfterThrow {
jketema marked this conversation as resolved.
Show resolved Hide resolved
ThrowExpr throw;

TranslatedDestructorsAfterThrow() { this = TTranslatedDestructorsAfterThrow(throw) }

override string toString() { result = "Destructor calls after throw: " + throw }

private TranslatedCall getTranslatedImplicitDestructorCall(int id) {
result.getExpr() = throw.getImplicitDestructorCall(id)
}

override Instruction getFirstInstruction(EdgeKind kind) {
result = this.getChild(0).getFirstInstruction(kind)
}

override ThrowExpr getAst() { result = throw }

override Instruction getInstructionSuccessorInternal(InstructionTag tag, EdgeKind kind) { none() }

override TranslatedElement getChild(int id) {
result = this.getTranslatedImplicitDestructorCall(id)
}

override predicate handlesDestructorsExplicitly() { any() }

override Declaration getFunction() { result = throw.getEnclosingFunction() }

override Instruction getChildSuccessorInternal(TranslatedElement child, EdgeKind kind) {
exists(int id | child = this.getChild(id) |
// Transition to the next child, if any.
result = this.getChild(id + 1).getFirstInstruction(kind)
or
// And otherwise, exit this element with an exceptional edge
not exists(this.getChild(id + 1)) and
kind instanceof ExceptionEdge and
result = this.getParent().getExceptionSuccessorInstruction(any(GotoEdge edge))
)
}

override TranslatedElement getLastChild() {
result =
this.getTranslatedImplicitDestructorCall(max(int id |
exists(throw.getImplicitDestructorCall(id))
))
}

override Instruction getALastInstructionInternal() {
result = this.getLastChild().getALastInstruction()
}

override predicate hasInstruction(Opcode opcode, InstructionTag tag, CppType resultType) {
none()
}
}

/**
* IR translation of a `throw` expression.
*/
Expand All @@ -2831,13 +2931,22 @@ abstract class TranslatedThrowExpr extends TranslatedNonConstantExpr {

override Instruction getInstructionSuccessorInternal(InstructionTag tag, EdgeKind kind) {
tag = ThrowTag() and
kind instanceof ExceptionEdge and
result = this.getParent().getExceptionSuccessorInstruction(any(GotoEdge edge))
(
result = this.getDestructors().getFirstInstruction(kind)
or
not exists(this.getDestructors()) and
kind instanceof ExceptionEdge and
result = this.getParent().getExceptionSuccessorInstruction(any(GotoEdge edge))
)
}

override Instruction getResult() { none() }

abstract Opcode getThrowOpcode();

override predicate handlesDestructorsExplicitly() { any() }

TranslatedDestructorsAfterThrow getDestructors() { result.getAst() = expr }
}

/**
Expand All @@ -2849,6 +2958,9 @@ class TranslatedThrowValueExpr extends TranslatedThrowExpr, TranslatedVariableIn

final override TranslatedElement getChildInternal(int id) {
result = TranslatedVariableInitialization.super.getChildInternal(id)
or
id = max(int i | exists(TranslatedVariableInitialization.super.getChildInternal(i))) + 1 and
result = this.getDestructors()
}

final override Instruction getChildSuccessorInternal(TranslatedElement elem, EdgeKind kind) {
Expand Down Expand Up @@ -2914,14 +3026,22 @@ class TranslatedThrowValueExpr extends TranslatedThrowExpr, TranslatedVariableIn
class TranslatedReThrowExpr extends TranslatedThrowExpr {
override ReThrowExpr expr;

override TranslatedElement getChildInternal(int id) { none() }
override TranslatedElement getChildInternal(int id) {
id = 0 and
result = this.getDestructors()
}

override Instruction getFirstInstruction(EdgeKind kind) {
result = this.getInstruction(ThrowTag()) and
kind instanceof GotoEdge
}

override Instruction getALastInstructionInternal() { result = this.getInstruction(ThrowTag()) }
override Instruction getALastInstructionInternal() {
result = this.getDestructors().getALastInstruction()
or
not this.hasAnImplicitDestructorCall() and
result = this.getInstruction(ThrowTag())
}

override Instruction getChildSuccessorInternal(TranslatedElement child, EdgeKind kind) { none() }

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
| test.cpp:680:30:680:30 | call to operator[] | This object is destroyed before $@ is called. | test.cpp:680:17:680:17 | call to begin | call to begin |
| test.cpp:680:30:680:30 | call to operator[] | This object is destroyed before $@ is called. | test.cpp:680:17:680:17 | call to end | call to end |
| test.cpp:683:31:683:32 | call to at | This object is destroyed before $@ is called. | test.cpp:683:17:683:17 | call to begin | call to begin |
| test.cpp:683:31:683:32 | call to at | This object is destroyed before $@ is called. | test.cpp:683:17:683:17 | call to end | call to end |
| test.cpp:689:17:689:29 | temporary object | This object is destroyed before $@ is called. | test.cpp:689:31:689:35 | call to begin | call to begin |
| test.cpp:689:46:689:58 | temporary object | This object is destroyed before $@ is called. | test.cpp:689:60:689:62 | call to end | call to end |
| test.cpp:702:27:702:27 | call to operator[] | This object is destroyed before $@ is called. | test.cpp:703:19:703:23 | call to begin | call to begin |
| test.cpp:702:27:702:27 | call to operator[] | This object is destroyed before $@ is called. | test.cpp:703:36:703:38 | call to end | call to end |
| test.cpp:716:36:716:48 | temporary object | This object is destroyed before $@ is called. | test.cpp:716:17:716:17 | call to begin | call to begin |
| test.cpp:716:36:716:48 | temporary object | This object is destroyed before $@ is called. | test.cpp:716:17:716:17 | call to end | call to end |
| test.cpp:727:23:727:23 | call to operator[] | This object is destroyed before $@ is called. | test.cpp:750:17:750:17 | call to begin | call to begin |
| test.cpp:727:23:727:23 | call to operator[] | This object is destroyed before $@ is called. | test.cpp:750:17:750:17 | call to end | call to end |
| test.cpp:735:23:735:23 | call to operator[] | This object is destroyed before $@ is called. | test.cpp:759:17:759:17 | call to begin | call to begin |
| test.cpp:735:23:735:23 | call to operator[] | This object is destroyed before $@ is called. | test.cpp:759:17:759:17 | call to end | call to end |
Original file line number Diff line number Diff line change
Expand Up @@ -677,10 +677,10 @@ std::vector<std::vector<int>> return_self_by_value(const std::vector<std::vector

void test() {
for (auto x : returnValue()) {} // GOOD
for (auto x : returnValue()[0]) {} // BAD [NOT DETECTED] (see *)
for (auto x : returnValue()[0]) {} // BAD
for (auto x : external_by_value(returnValue())) {} // GOOD
for (auto x : external_by_const_ref(returnValue())) {} // GOOD
for (auto x : returnValue().at(0)) {} // BAD [NOT DETECTED] (see *)
for (auto x : returnValue().at(0)) {} // BAD

for (auto x : returnRef()) {} // GOOD
for (auto x : returnRef()[0]) {} // GOOD
Expand All @@ -700,7 +700,7 @@ void test() {

{
auto&& v = returnValue()[0];
for(auto it = v.begin(); it != v.end(); ++it) {} // BAD [NOT DETECTED] (see *)
for(auto it = v.begin(); it != v.end(); ++it) {} // BAD
}

{
Expand All @@ -713,7 +713,7 @@ void test() {
for(auto it = v.begin(); it != v.end(); ++it) {} // GOOD
}

for (auto x : return_self_by_ref(returnValue())) {} // BAD [NOT DETECTED] (see *)
for (auto x : return_self_by_ref(returnValue())) {} // BAD

for (auto x : return_self_by_value(returnValue())) {} // GOOD
}
Expand All @@ -724,15 +724,15 @@ void iterate(const std::vector<T>& v) {
}

std::vector<int>& ref_to_first_in_returnValue_1() {
return returnValue()[0]; // BAD [NOT DETECTED] (see *)
return returnValue()[0]; // BAD
}

std::vector<int>& ref_to_first_in_returnValue_2() {
return returnValue()[0]; // BAD [NOT DETECTED]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this case not detected? The function signature and implementation is identical to ref_to_first_in_returnValue_1

Copy link
Contributor Author

@MathiasVP MathiasVP Apr 9, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't investigated why yet. Note that, while function signature and implementation is identical to ref_to_first_in_returnValue_1, they differ in how they're used. See here for this. Specifically:

In the one we detect (i.e., ref_to_first_in_returnValue_1) the range-based for loop looks like:

for (auto x : ref_to_first_in_returnValue_1()) {}

and in the one we fail to detect the range-based for loop looks like:

{
auto value = ref_to_first_in_returnValue_2();
for (auto x : value) {}
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could improve the UX on this by moving the alert location to the for-loop (instead of at the place where the temporary is destructed). The problem with this is that the elements in the DB for range-based for loops don't always have a location (which makes for an even worse UX 😂)

}

std::vector<int>& ref_to_first_in_returnValue_3() {
return returnValue()[0]; // BAD [NOT DETECTED] (see *)
return returnValue()[0]; // BAD
}

std::vector<int> first_in_returnValue_1() {
Expand Down
Loading
Loading