llvm / llvm/llvm-project

[clang] Coroutines: lambda object is passed by reference instead of by value

Open
#214,434 5 comments 0 reactions 0 assignees View on GitHub
coroutines
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.