-
Notifications
You must be signed in to change notification settings - Fork 11k
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
[coroutines] clang fails to run trivial ABI destructors when a suspended coroutine is destroyed #88478
Comments
It does seem like a bug, the presence of @usx95 this seems similar to the bug you fixed where destructors did not run. Could you take a look at this one? |
@llvm/issue-subscribers-coroutines Author: Aaron Jacobs (jacobsa)
Here's a small program containing a coroutine (`Foo`) that starts to evaluate a function call expression but then destroys itself before actually making the function call:
#include <coroutine>
#include <cstdlib>
#include <iostream>
struct DestroySelfTag {};
// An eager coroutine result type that supports awaiting DestroySelf to
// cause the promise to destroy the coroutine frame.
struct MyTask {
struct promise_type {
MyTask get_return_object() { return {}; }
std::suspend_never initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
void unhandled_exception();
void return_void();
auto await_transform(DestroySelfTag) {
struct Awaiter {
bool await_ready() { return false; }
void await_suspend(const std::coroutine_handle<> h) { h.destroy(); }
int await_resume() {
// We should never resume; we destroyed ourselves just after
// suspending.
std::abort();
}
};
return Awaiter{};
}
};
};
// A trivial ABI type that lets us know when it's created and destroyed.
struct HasDestructor {
HasDestructor() { std::cout << "created: " << this << "\n"; }
~HasDestructor() { std::cout << "destroyed: " << this << "\n"; }
};
// A function that we have a call expression for below in Foo. This just
// needs to accept the right types -- it should never actually be called.
void AcceptArgs(HasDestructor, int, HasDestructor) { std::abort(); }
// A coroutine that creates a HasDestructor object then destroys itself
// before doing anything with it.
MyTask Foo() {
AcceptArgs(HasDestructor(), co_await DestroySelfTag(), HasDestructor());
std::abort();
}
int main() {
Foo();
return 0;
} When compiled with
However, the program is miscompiled when we put the
It doesn't seem like this should happen. There isn't extremely rigorous documentation on the semantics of MyTask Foo() {
HasDestructor(), co_await DestroySelfTag();
std::abort();
} In a real codebase this bug could cause resources to be leaked when a coroutine is stopped early, e.g. due to cancellation. |
Reproduces with early exit in statement expression and without void AcceptArgs(HasDestructor, int, HasDestructor) {}
void Foo() {
AcceptArgs(HasDestructor(), ({return; 0;}), HasDestructor());
} |
I agree, that is reassuring. I was going to say that's the case we need to check. There are a few places like this where clang has a scheme to push EH-only cleanups, and then disable or pop them off the stack after the call transferring ownership occurs, which seems related. |
Sent out fix: #88751 |
@llvm/issue-subscribers-clang-codegen Author: Aaron Jacobs (jacobsa)
Here's a small program containing a coroutine (`Foo`) that starts to evaluate a function call expression but then destroys itself before actually making the function call:
#include <coroutine>
#include <cstdlib>
#include <iostream>
struct DestroySelfTag {};
// An eager coroutine result type that supports awaiting DestroySelf to
// cause the promise to destroy the coroutine frame.
struct MyTask {
struct promise_type {
MyTask get_return_object() { return {}; }
std::suspend_never initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
void unhandled_exception();
void return_void();
auto await_transform(DestroySelfTag) {
struct Awaiter {
bool await_ready() { return false; }
void await_suspend(const std::coroutine_handle<> h) { h.destroy(); }
int await_resume() {
// We should never resume; we destroyed ourselves just after
// suspending.
std::abort();
}
};
return Awaiter{};
}
};
};
// A trivial ABI type that lets us know when it's created and destroyed.
struct HasDestructor {
HasDestructor() { std::cout << "created: " << this << "\n"; }
~HasDestructor() { std::cout << "destroyed: " << this << "\n"; }
};
// A function that we have a call expression for below in Foo. This just
// needs to accept the right types -- it should never actually be called.
void AcceptArgs(HasDestructor, int, HasDestructor) { std::abort(); }
// A coroutine that creates a HasDestructor object then destroys itself
// before doing anything with it.
MyTask Foo() {
AcceptArgs(HasDestructor(), co_await DestroySelfTag(), HasDestructor());
std::abort();
}
int main() {
Foo();
return 0;
} When compiled with
However, the program is miscompiled when we put the
It doesn't seem like this should happen. There isn't extremely rigorous documentation on the semantics of MyTask Foo() {
HasDestructor(), co_await DestroySelfTag();
std::abort();
} In a real codebase this bug could cause resources to be leaked when a coroutine is stopped early, e.g. due to cancellation. |
(the primary commit for stmt-expr cleanups was reverted) |
Updated PR: #89154 |
) 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
Here's a small program containing a coroutine (
Foo
) that starts to evaluate a function call expression but then destroys itself before actually making the function call:When compiled with
-std=c++20 -O2 -fno-exceptions
(Compiler Explorer), the program works as expected. It creates oneHasDestructor
object, then destroys it again whenstd::coroutine_handle::destroy
is called:However, the program is miscompiled when we put the
[[clang::trivial_abi]]
attribute onHasDestructor
(Compiler Explorer). In this case we fail to run the destructor, creating but then not ever destroying theHasDestructor
object:It doesn't seem like this should happen. There isn't extremely rigorous documentation on the semantics of
[[clang::trivial_abi]]
, but what does exist says "the convention is that the callee will destroy the object before returning". That makes sense, but there is no function call here.AcceptArgs
is never actually called. Instead this should be treated the same as the following, which does work correctly:In a real codebase this bug could cause resources to be leaked when a coroutine is stopped early, e.g. due to cancellation.
The text was updated successfully, but these errors were encountered: