Skip to content

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

@jacobsa

Description

@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 -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.

Metadata

Metadata

Assignees

Labels

clang:codegenIR generation bugs: mangling, exceptions, etc.coroutinesC++20 coroutines

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions