[clang] Coroutines: lambda object is passed by reference instead of by value
- Dominant language
- LLVM
- Stars
- 40.5k
- Forks
- 18.7k
- PR merge metrics
- PR metrics pending
Description
Binaries produced with `clang++ -std=c++23 -O2` spin perpetually, while `-O0`/`-O1` pass fine.
The clang frontend generates correct IR, so the bug is introduced during LLVM lowering.
## Minimal reproducer ([`minimal.cpp`](https://github.com/user-attachments/files/30779728/minimal.cpp))
```cpp
#include
struct M;
struct A {
M* m;
bool await_ready() noexcept;
void await_suspend(std::coroutine_handle<>) noexcept {}
void await_resume() noexcept {}
};
struct M { unsigned st = 0; A lock() noexcept { return A{this}; } };
bool A::await_ready() noexcept {
while (__atomic_exchange_n(&m->st, 1, __ATOMIC_ACQ_REL))
;
return true;
}
struct T {
struct promise_type {
T get_return_object() noexcept { return {std::coroutine_handle::from_promise(*this)}; }
std::suspend_always initial_suspend() noexcept { return {}; }
std::suspend_never final_suspend() noexcept { return {}; }
void return_void() noexcept {}
void unhandled_exception() noexcept {}
};
std::coroutine_handle h;
};
int main() {
M m;
auto t = [&]() -> T { co_await m.lock(); co_return; }();
t.h.resume();
}
```
A coroutine lambda captures `m` by reference and `co_await`s `m.lock()` (a member function returning the awaiter by value).
The spin loop in `await_ready` is required to trigger the bug.
## Reproduction
```sh
clang++ -std=c++23 -O2 minimal.cpp -o a.out && timeout 10 ./a.out # hangs for 10s
clang++ -std=c++23 -O0 minimal.cpp -o a.out && timeout 10 ./a.out # exits
```
## Analysis of the codegen
The issue originates from the coroutine resume function;
The capture is reloaded from the frame and dereferenced again,
so the spinlock target becomes the capture's contents instead of the capture address:
```llvm
%.reload.addr = getelementptr inbounds i8, ptr %0, i64 24
%.reload = load ptr, ptr %.reload.addr, align 8
%1 = load ptr, ptr %.reload, align 8 ; extra deref
%2 = atomicrmw xchg ptr %1, i32 1 acq_rel ; spins forever on the wrong address
```
The frontend's equivalent access is a single load of the capture member.
## Pass region
The bad codegen appears sometime during coroutine splitting / early optimization.
Its still correct after `coro-split,simplifycfg`, corrupted once `sroa` runs and fully folded after `early-cse`.
## Environment
clang version 22.1.8 (Fedora 22.1.8-4.fc44), x86_64-redhat-linux-gnu, Fedora Linux 44, Intel Core Ultra 7 265U.
# Edit
This turned out to be a clang bug, not an LLVM lowering bug. While the C++ code provided should be conformant in my interpretation of the standard, the lambda object is passed by reference and not copied, which makes it not live at least as long as the promise. See the comment below for details
Contributor guide
Research direction
Start with the minimal.cpp reproducer and run the supplied -O2 and -O0 commands. Inspect the coroutine resume function and compare IR through coro-split,simplifycfg, sroa, and early-cse, focusing on the capture reload. Done means the optimized build no longer hangs and the regression is covered by an appropriate test.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- cpp
- Domain
- compilers
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 43/100