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

destructors not always properly run when a coroutine is suspended and then destroyed #63818

Closed
zygoloid opened this issue Jul 12, 2023 · 20 comments · Fixed by #85398 or #89154
Closed

destructors not always properly run when a coroutine is suspended and then destroyed #63818

zygoloid opened this issue Jul 12, 2023 · 20 comments · Fixed by #85398 or #89154

Comments

@zygoloid
Copy link
Collaborator

zygoloid commented Jul 12, 2023

Example:

#include <coroutine>
#include <iostream>

struct coroutine {
  struct promise_type;
  std::coroutine_handle<promise_type> handle;
  ~coroutine() { handle.destroy(); }
};

struct coroutine::promise_type {
  coroutine get_return_object() {
    return {std::coroutine_handle<promise_type>::from_promise(*this)};
  }
  std::suspend_never initial_suspend() noexcept { return {}; }
  std::suspend_never final_suspend() noexcept { return {}; }
  void return_void() {}
  void unhandled_exception() {}
};

struct Printy {
  Printy(const char *name) : name(name) { std::cout << "Printy(" << name << ")\n"; }
  Printy(const Printy&) = delete;
  ~Printy() { std::cout << "~Printy(" << name << ")\n"; }
  const char *name;
};

int main() {
  [] -> coroutine {
    Printy a("a");
    Printy arr[] = {
      Printy("b"), Printy("c"),
      (co_await std::suspend_always{}, Printy("d")),
      Printy("e")
    };
  }();
}

When the coroutine is destroyed after being suspended, a is destroyed, but arr[0] and arr[1] are not. Clang does not in general properly create cleanups for non-exceptional control flow that occurs in the middle of an expression / an initializer. At least array initialization is missing cleanups here, but it'd be worth checking through all exception-only cleanups because most of them are probably incorrect. It looks like there are 15 places where we currently push an EH-only cleanup:

CGCall.cpp:      pushFullExprCleanup<DestroyUnpassedArg>(EHCleanup, Slot.getAddress(),
CGClass.cpp:    CGF.EHStack.pushCleanup<CallBaseDtor>(EHCleanup, BaseClassDecl,
CGClass.cpp:    EHStack.pushCleanup<CallDelegatingCtorDtor>(EHCleanup,
CGCoroutine.cpp:    EHStack.pushCleanup<CallCoroEnd>(EHCleanup);
CGDecl.cpp:  pushDestroy(EHCleanup, addr, type, getDestroyer(dtorKind), true);
CGDecl.cpp:  pushFullExprCleanup<IrregularPartialArrayDestroy>(EHCleanup,
CGDecl.cpp:  pushFullExprCleanup<RegularPartialArrayDestroy>(EHCleanup,
CGException.cpp:  pushFullExprCleanup<FreeException>(EHCleanup, addr.getPointer());
CGExprAgg.cpp:        CGF.pushDestroy(EHCleanup, LV.getAddress(CGF), CurField->getType(),
CGExprAgg.cpp:        CGF.pushDestroy(EHCleanup, LV.getAddress(CGF), field->getType(),
CGExprCXX.cpp:      .pushCleanupWithExtra<DirectCleanup>(EHCleanup,
CGExprCXX.cpp:    .pushCleanupWithExtra<ConditionalCleanup>(EHCleanup,
ItaniumCXXABI.cpp:    CGF.EHStack.pushCleanup<CallGuardAbort>(EHCleanup, guard);
MicrosoftCXXABI.cpp:    CGF.EHStack.pushCleanup<ResetGuardBit>(EHCleanup, GuardAddr, GuardNum);
MicrosoftCXXABI.cpp:    CGF.EHStack.pushCleanup<CallInitThreadAbort>(EHCleanup, GuardAddr);

This bug is not new with coroutines; the same thing happens with statement expressions:

int main() {
  Printy arr[] = { Printy("a"), ({ return 0; Printy("b"); }) };
}

... never destroys arr[0]. But it seems more pressing now that it's reachable from standard C++20 code.

@llvmbot
Copy link
Collaborator

llvmbot commented Jul 12, 2023

@llvm/issue-subscribers-c-20

@llvmbot
Copy link
Collaborator

llvmbot commented Jul 12, 2023

@llvm/issue-subscribers-coroutines

@yuxuanchen1997
Copy link
Contributor

I am taking a look at this now.

@yuxuanchen1997 yuxuanchen1997 self-assigned this Dec 8, 2023
@yuxuanchen1997
Copy link
Contributor

For those who are reading this, this issue has nothing to do with the less common comma operator syntax shown in the example. If you have a PrintyAwaiter here in the much more common scenario you exhibit the same bug: ~Printy("b") is not run.

task test() {
  Printy a("a");
  Printies p{
    Printy("b"),
    co_await PrintyAwaiter("c"),
    Printy("d"),
  };
  co_return;
}

Also, yes, aggregate initialize is also list initialize. Confirmed to have the same bug.

@yuxuanchen1997
Copy link
Contributor

CodeGenFunction::EmitBranchThroughCleanup ignores cleanups that are not Normal. Simply changing the pushDestroy to push a NormalAndEHCleanup here doesn't seem to work. It calls more destructors than it should.

@usx95
Copy link
Contributor

usx95 commented Jan 19, 2024

Interestingly, this is not the case with temporaries associated with function call parameters. This only happens with list/aggregate initialisation.

coroutine foo() {
    Printy a("a");
    // bar(Printy("b"), co_await Awaiter{}); // Fine.

    // (Printy("b"), co_await Awaiter{}); // Fine.

    // Printies p1(Printy("b"),  // Fine.
    //             co_await Awaiter{});

    Printies p2{Printy("b"),  // Leak.
                co_await Awaiter{}};
    co_return;
}

https://godbolt.org/z/x9zKKsoY8

@alanzhao1
Copy link
Contributor

alanzhao1 commented Jan 22, 2024

I took a quick stab at this. The following diff resolves the example in the description that doesn't have a coroutine:

diff --git a/clang/lib/CodeGen/CGDecl.cpp b/clang/lib/CodeGen/CGDecl.cpp
index a5da0aa2965a..7c961ef91e6d 100644
--- a/clang/lib/CodeGen/CGDecl.cpp
+++ b/clang/lib/CodeGen/CGDecl.cpp
@@ -2423,7 +2423,7 @@ void CodeGenFunction::pushIrregularPartialArrayCleanup(llvm::Value *arrayBegin,
                                                        QualType elementType,
                                                        CharUnits elementAlign,
                                                        Destroyer *destroyer) {
-  pushFullExprCleanup<IrregularPartialArrayDestroy>(EHCleanup,
+  pushFullExprCleanup<IrregularPartialArrayDestroy>(NormalAndEHCleanup,
                                                     arrayBegin, arrayEndPointer,
                                                     elementType, elementAlign,
                                                     destroyer);

Output:

Printy(a)
~Printy(a)

But that diff causes the example with the coroutine to segfault (when compiled with -stdlib=libc++):

Printy(a)
Printy(b)
Printy(c)
[1]    1813892 segmentation fault  /tmp/coroutine-dtor-maybefixed

@alanzhao1
Copy link
Contributor

Also, observe that gcc doesn't construct Printy(b) and Printy(c) at all: https://godbolt.org/z/hb85j4YzW

@usx95
Copy link
Contributor

usx95 commented Jan 23, 2024

Yeah I noticed. In the following example, GCC creates b and c only after the final resumption in the expr and not in the lexical order. godbolt

Printies p2{Printy("b"), co_await Awaiter{"a1"}, Printy("c"), co_await Awaiter{"a2"}};

Clang:

Printy(b)
Coroutine suspended
Coroutine resumed:
Printy(a1)
Printy(c)
Coroutine suspended
Coroutine resumed:
Printy(a2)
~Printy(a2)
~Printy(c)
~Printy(a1)
~Printy(b)
Coroutine destroyed

GCC:

Coroutine suspended
Coroutine resumed:
Coroutine suspended
Coroutine resumed:
Printy(b)
Printy(a1)
Printy(c)
Printy(a2)
~Printy(a2)
~Printy(c)
~Printy(a1)
~Printy(b)
Coroutine destroyed

@usx95 usx95 self-assigned this Jan 23, 2024
@usx95
Copy link
Contributor

usx95 commented Jan 24, 2024

I think now I understand the root cause and why simply changing the cleanup type would not solve the issue.
This is because of the lifetime extension of temporaries in list-init, braced-init, and backing arrays.

struct Printies { const Printy &a; };

int main() {
    Printies p1(Printy("a")); // dangling.
    std::cout << "p1 ctor completed\n";
    Printies p2{Printy("b")}; // lifetime extended to p2.
    std::cout << "p2 ctor completed\n";
    Printy arr[] = {Printy("c"), Printy("d")}; // lifetimes extended to arr.
    std::cout << "arr[] ctor completed\n";
    return 0;
}

produces (clang is correct here): godbolt

Printy(a)
~Printy(a)
p1 ctor completed
Printy(b)
p2 ctor completed
Printy(c)
Printy(d)
arr[] ctor completed
~Printy(d) <-------- Lifetime extended
~Printy(c) <-------- Lifetime extended
~Printy(b) <-------- Lifetime extended

In the above example, the lifetime of temporary Printy("b") is extended to p2, i.e., it is destroyed when p2 is destroyed. If we have control flow in such expressions (through coroutine suspensions or statement expressions), we would miss cleaning the temporary because we would not want to destroy p2 since it is not yet constructed.

@ChuanqiXu9
Copy link
Member

Thanks to the analysis of @usx95

There is another example not relevant with arrays: https://godbolt.org/z/4rnTj53qf. The key point here is that the construction is interrupted but not by exceptions.

@usx95
Copy link
Contributor

usx95 commented Jan 25, 2024

I do not understand the non-array example. It does not have temporaries with missed dtor.
Moreover this is not the case with parenthesised-init and only braced-init (because of the lifetime ext) https://godbolt.org/z/GEo3fd89T.

We do not have this problem with such control-flow with exceptions (https://godbolt.org/z/T9scoehev).
We should target performing the same cleanups atleast in coroutine::destroy (if not something generic to solve return in stmt expr as well). Basically EHCleanup should be cleaned in destroy as well.

@ChuanqiXu9
Copy link
Member

ChuanqiXu9 commented Jan 25, 2024

I do not understand the non-array example. It does not have temporaries with missed dtor. Moreover this is not the case with parenthesised-init and only braced-init (because of the lifetime ext) https://godbolt.org/z/GEo3fd89T.

Oh, yeah, you're correct. Sorry for misleading : (

@llvmbot
Copy link
Collaborator

llvmbot commented Feb 5, 2024

@llvm/issue-subscribers-clang-codegen

Author: Richard Smith (zygoloid)

[Example](https://godbolt.org/z/zezvTqY8v):
#include &lt;coroutine&gt;
#include &lt;iostream&gt;

struct coroutine {
  struct promise_type;
  std::coroutine_handle&lt;promise_type&gt; handle;
  ~coroutine() { handle.destroy(); }
};

struct coroutine::promise_type {
  coroutine get_return_object() {
    return {std::coroutine_handle&lt;promise_type&gt;::from_promise(*this)};
  }
  std::suspend_never initial_suspend() noexcept { return {}; }
  std::suspend_never final_suspend() noexcept { return {}; }
  void return_void() {}
  void unhandled_exception() {}
};

struct Printy {
  Printy(const char *name) : name(name) { std::cout &lt;&lt; "Printy(" &lt;&lt; name &lt;&lt; ")\n"; }
  Printy(const Printy&amp;) = delete;
  ~Printy() { std::cout &lt;&lt; "~Printy(" &lt;&lt; name &lt;&lt; ")\n"; }
  const char *name;
};

int main() {
  [] -&gt; coroutine {
    Printy a("a");
    Printy arr[] = {
      Printy("b"), Printy("c"),
      (co_await std::suspend_always{}, Printy("d")),
      Printy("e")
    };
  }();
}

When the coroutine is destroyed after being suspended, a is destroyed, but arr[0] and arr[1] are not. Clang does not in general properly create cleanups for non-exceptional control flow that occurs in the middle of an expression / an initializer. At least array initialization is missing cleanups here, but it'd be worth checking through all exception-only cleanups because most of them are probably incorrect. It looks like there are 15 places where we currently push an EH-only cleanup:

CGCall.cpp:      pushFullExprCleanup&lt;DestroyUnpassedArg&gt;(EHCleanup, Slot.getAddress(),
CGClass.cpp:    CGF.EHStack.pushCleanup&lt;CallBaseDtor&gt;(EHCleanup, BaseClassDecl,
CGClass.cpp:    EHStack.pushCleanup&lt;CallDelegatingCtorDtor&gt;(EHCleanup,
CGCoroutine.cpp:    EHStack.pushCleanup&lt;CallCoroEnd&gt;(EHCleanup);
CGDecl.cpp:  pushDestroy(EHCleanup, addr, type, getDestroyer(dtorKind), true);
CGDecl.cpp:  pushFullExprCleanup&lt;IrregularPartialArrayDestroy&gt;(EHCleanup,
CGDecl.cpp:  pushFullExprCleanup&lt;RegularPartialArrayDestroy&gt;(EHCleanup,
CGException.cpp:  pushFullExprCleanup&lt;FreeException&gt;(EHCleanup, addr.getPointer());
CGExprAgg.cpp:        CGF.pushDestroy(EHCleanup, LV.getAddress(CGF), CurField-&gt;getType(),
CGExprAgg.cpp:        CGF.pushDestroy(EHCleanup, LV.getAddress(CGF), field-&gt;getType(),
CGExprCXX.cpp:      .pushCleanupWithExtra&lt;DirectCleanup&gt;(EHCleanup,
CGExprCXX.cpp:    .pushCleanupWithExtra&lt;ConditionalCleanup&gt;(EHCleanup,
ItaniumCXXABI.cpp:    CGF.EHStack.pushCleanup&lt;CallGuardAbort&gt;(EHCleanup, guard);
MicrosoftCXXABI.cpp:    CGF.EHStack.pushCleanup&lt;ResetGuardBit&gt;(EHCleanup, GuardAddr, GuardNum);
MicrosoftCXXABI.cpp:    CGF.EHStack.pushCleanup&lt;CallInitThreadAbort&gt;(EHCleanup, GuardAddr);

This bug is not new with coroutines; the same thing happens with statement expressions:

int main() {
  Printy arr[] = { Printy("a"), ({ return 0; Printy("b"); }) };
}

... never destroys arr[0]. But it seems more pressing now that it's reachable from standard C++20 code.

@usx95
Copy link
Contributor

usx95 commented Feb 5, 2024

Sent out partial resolution : #80698

List initialisation still has missing cleanups.

Edit: Updated the patch to target all the missing cleanups.

@usx95
Copy link
Contributor

usx95 commented Apr 17, 2024

(previous change had to be reverted)

@usx95 usx95 reopened this Apr 17, 2024
@Alcaro
Copy link
Contributor

Alcaro commented Apr 17, 2024

For those wondering, the revert is #88884 (which also reverts #88478 )

@usx95
Copy link
Contributor

usx95 commented Apr 17, 2024

Updated PR: #89154

@usx95 usx95 reopened this Apr 22, 2024
usx95 added a commit that referenced this issue Apr 29, 2024
)

Latest diff:
https://github.com/llvm/llvm-project/pull/89154/files/f1ab4c2677394bbfc985d9680d5eecd7b2e6a882..adf9bc902baddb156c83ce0f8ec03c142e806d45

We address two additional bugs here: 

### Problem 1: Deactivated normal cleanup still runs, leading to
double-free
Consider the following:
```cpp

struct A { };

struct B { B(const A&); };

struct S {
  A a;
  B b;
};

int AcceptS(S s);

void Accept2(int x, int y);

void Test() {
  Accept2(AcceptS({.a = A{}, .b = A{}}), ({ return; 0; }));
}
```
We add cleanups as follows:
1. push dtor for field `S::a`
2. push dtor for temp `A{}` (used by ` B(const A&)` in `.b = A{}`)
3. push dtor for field `S::b`
4. Deactivate 3 `S::b`-> This pops the cleanup.
5. Deactivate 1 `S::a` -> Does not pop the cleanup as *2* is top. Should
create _active flag_!!
6. push dtor for `~S()`.
7. ...

It is important to deactivate **5** using active flags. Without the
active flags, the `return` will fallthrough it and would run both `~S()`
and dtor `S::a` leading to **double free** of `~A()`.
In this patch, we unconditionally emit active flags while deactivating
normal cleanups. These flags are deleted later by the `AllocaTracker` if
the cleanup is not emitted.

### Problem 2: Missing cleanup for conditional lifetime extension
We push 2 cleanups for lifetime-extended cleanup. The first cleanup is
useful if we exit from the middle of the expression (stmt-expr/coro
suspensions). This is deactivated after full-expr, and a new cleanup is
pushed, extending the lifetime of the temporaries (to the scope of the
reference being initialized).
If this lifetime extension happens to be conditional, then we use active
flags to remember whether the branch was taken and if the object was
initialized.
Previously, we used a **single** active flag, which was used by both
cleanups. This is wrong because the first cleanup will be forced to
deactivate after the full-expr and therefore this **active** flag will
always be **inactive**. The dtor for the lifetime extended entity would
not run as it always sees an **inactive** flag.

In this patch, we solve this using two separate active flags for both
cleanups. Both of them are activated if the conditional branch is taken,
but only one of them is deactivated after the full-expr.

---

Fixes #63818
Fixes #88478

---

Previous PR logs:
1. #85398
2. #88670
3. #88751
4. #88884
@higher-performance
Copy link
Contributor

@usx95 I think there may still be a bug? Unless I'm mistaken & the trunk builds I'm trying aren't, in fact, the trunk.

See here: #91761

@usx95
Copy link
Contributor

usx95 commented May 10, 2024

Thanks for pointing out. This does seem like another blocker for stmt-expr but is not effecting C++20 coroutines. So let's keep this bug as closed and fixed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment