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

[coroutines] clang fails to run trivial ABI destructors when a suspended coroutine is destroyed #88478

Closed
jacobsa opened this issue Apr 12, 2024 · 9 comments · Fixed by #88751 or #89154
Assignees
Labels

Comments

@jacobsa
Copy link
Contributor

jacobsa commented Apr 12, 2024

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 -std=c++20 -O2 -fno-exceptions (Compiler Explorer), the program works as expected. It creates one HasDestructor object, then destroys it again when std::coroutine_handle::destroy is called:

created:   0x7ffd04aa2b6a
destroyed: 0x7ffd04aa2b6a

However, the program is miscompiled when we put the [[clang::trivial_abi]] attribute on HasDestructor (Compiler Explorer). In this case we fail to run the destructor, creating but then not ever destroying the HasDestructor object:

created:   0x7fff8f4da6b2

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:

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.

@github-actions github-actions bot added the clang Clang issues not falling into any other category label Apr 12, 2024
@ilya-biryukov
Copy link
Contributor

It does seem like a bug, the presence of [[clang::trivial_abi]] should not inhibit destructor calls here.
Similar example with exceptions does work: https://godbolt.org/z/j1Gqo9dax.

@usx95 this seems similar to the bug you fixed where destructors did not run. Could you take a look at this one?

@usx95 usx95 self-assigned this Apr 12, 2024
@usx95 usx95 added the coroutines C++20 coroutines label Apr 12, 2024
@llvmbot
Copy link
Collaborator

llvmbot commented Apr 12, 2024

@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 &lt;coroutine&gt;
#include &lt;cstdlib&gt;
#include &lt;iostream&gt;

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&lt;&gt; 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 &lt;&lt; "created:   " &lt;&lt; this &lt;&lt; "\n"; }
  ~HasDestructor() { std::cout &lt;&lt; "destroyed: " &lt;&lt; this &lt;&lt; "\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 -std=c++20 -O2 -fno-exceptions (Compiler Explorer), the program works as expected. It creates one HasDestructor object, then destroys it again when std::coroutine_handle::destroy is called:

created:   0x7ffd04aa2b6a
destroyed: 0x7ffd04aa2b6a

However, the program is miscompiled when we put the [[clang::trivial_abi]] attribute on HasDestructor (Compiler Explorer). In this case we fail to run the destructor, creating but then not ever destroying the HasDestructor object:

created:   0x7fff8f4da6b2

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:

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.

@usx95
Copy link
Contributor

usx95 commented Apr 12, 2024

Reproduces with early exit in statement expression and without std::abort. So this is very likely an edge case of #63818.
The fact that it works with exceptions is reassuring. I will take a look at it.

void AcceptArgs(HasDestructor, int, HasDestructor) {}

void Foo() {
    AcceptArgs(HasDestructor(), ({return; 0;}), HasDestructor());
}

compiler explorer

@usx95 usx95 changed the title [coroutines] clang fails to run trivial ABI destructors for an "aborted" function call [coroutines] clang fails to run trivial ABI destructors when a suspended coroutine is destroyed Apr 12, 2024
@EugeneZelenko EugeneZelenko removed the clang Clang issues not falling into any other category label Apr 12, 2024
@rnk
Copy link
Collaborator

rnk commented Apr 15, 2024

The fact that it works with exceptions is reassuring.

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.

@usx95
Copy link
Contributor

usx95 commented Apr 15, 2024

Sent out fix: #88751

@llvmbot
Copy link
Collaborator

llvmbot commented Apr 16, 2024

@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 &lt;coroutine&gt;
#include &lt;cstdlib&gt;
#include &lt;iostream&gt;

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&lt;&gt; 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 &lt;&lt; "created:   " &lt;&lt; this &lt;&lt; "\n"; }
  ~HasDestructor() { std::cout &lt;&lt; "destroyed: " &lt;&lt; this &lt;&lt; "\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 -std=c++20 -O2 -fno-exceptions (Compiler Explorer), the program works as expected. It creates one HasDestructor object, then destroys it again when std::coroutine_handle::destroy is called:

created:   0x7ffd04aa2b6a
destroyed: 0x7ffd04aa2b6a

However, the program is miscompiled when we put the [[clang::trivial_abi]] attribute on HasDestructor (Compiler Explorer). In this case we fail to run the destructor, creating but then not ever destroying the HasDestructor object:

created:   0x7fff8f4da6b2

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:

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.

@usx95
Copy link
Contributor

usx95 commented Apr 17, 2024

(the primary commit for stmt-expr cleanups was reverted)

@Alcaro
Copy link
Contributor

Alcaro commented Apr 17, 2024

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

@usx95
Copy link
Contributor

usx95 commented Apr 17, 2024

Updated PR: #89154

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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
7 participants