mirror of
https://github.com/llvm/llvm-project.git
synced 2025-04-17 04:16:46 +00:00

### Problem ```cpp co_task<int> coro() { int a = 1; auto lamb = [a]() -> co_task<int> { co_return a; // 'a' in the lambda object dies after the iniital_suspend in the lambda coroutine. }(); co_return co_await lamb; } ``` [use-after-free](https://godbolt.org/z/GWPEovWWc) Lambda captures (even by value) are prone to use-after-free once the lambda object dies. In the above example, the lambda object appears only as a temporary in the call expression. It dies after the first suspension (`initial_suspend`) in the lambda. On resumption in `co_await lamb`, the lambda accesses `a` which is part of the already-dead lambda object. --- ### Solution This problem can be formulated by saying that the `this` parameter of the lambda call operator is a lifetimebound parameter. The lambda object argument should therefore live atleast as long as the return object. That said, this requirement does not hold if the lambda does not have a capture list. In principle, the coroutine frame still has a reference to a dead lambda object, but it is easy to see that the object would not be used in the lambda-coroutine body due to no capture list. It is safe to use this pattern inside a`co_await` expression due to the lifetime extension of temporaries. Example: ```cpp co_task<int> coro() { int a = 1; int res = co_await [a]() -> co_task<int> { co_return a; }(); co_return res; } ``` --- ### Background This came up in the discussion with seastar folks on [RFC](https://discourse.llvm.org/t/rfc-lifetime-bound-check-for-parameters-of-coroutines/74253/19?u=usx95). This is a fairly common pattern in continuation-style-passing (CSP) async programming involving futures and continuations. Document ["Lambda coroutine fiasco"](https://github.com/scylladb/seastar/blob/master/doc/lambda-coroutine-fiasco.md) by Seastar captures the problem. This pattern makes the migration from CSP-style async programming to coroutines very bugprone. Fixes https://github.com/llvm/llvm-project/issues/76995 --------- Co-authored-by: Chuanqi Xu <yedeng.yd@linux.alibaba.com>
200 lines
6.8 KiB
C++
200 lines
6.8 KiB
C++
// RUN: %clang_cc1 -triple x86_64-apple-darwin9 %s -std=c++20 -fsyntax-only -verify -Wall -Wextra -Wno-error=unreachable-code -Wno-unused -Wno-c++23-lambda-attributes
|
|
|
|
#include "Inputs/std-coroutine.h"
|
|
|
|
using std::suspend_always;
|
|
using std::suspend_never;
|
|
|
|
template <typename T> struct [[clang::coro_lifetimebound, clang::coro_return_type]] Co {
|
|
struct promise_type {
|
|
Co<T> get_return_object() {
|
|
return {};
|
|
}
|
|
suspend_always initial_suspend();
|
|
suspend_always final_suspend() noexcept;
|
|
void unhandled_exception();
|
|
void return_value(const T &t);
|
|
|
|
template <typename U>
|
|
auto await_transform(const Co<U> &) {
|
|
struct awaitable {
|
|
bool await_ready() noexcept { return false; }
|
|
void await_suspend(std::coroutine_handle<>) noexcept {}
|
|
U await_resume() noexcept { return {}; }
|
|
};
|
|
return awaitable{};
|
|
}
|
|
};
|
|
};
|
|
|
|
Co<int> foo_coro(const int& b) {
|
|
if (b > 0)
|
|
co_return 1;
|
|
co_return 2;
|
|
}
|
|
|
|
int getInt() { return 0; }
|
|
|
|
Co<int> bar_coro(const int &b, int c) {
|
|
int x = co_await foo_coro(b);
|
|
int y = co_await foo_coro(1);
|
|
int z = co_await foo_coro(getInt());
|
|
auto unsafe1 = foo_coro(1); // expected-warning {{temporary whose address is used as value of local variable}}
|
|
auto unsafe2 = foo_coro(getInt()); // expected-warning {{temporary whose address is used as value of local variable}}
|
|
auto safe1 = foo_coro(b);
|
|
auto safe2 = foo_coro(c);
|
|
co_return co_await foo_coro(co_await foo_coro(1));
|
|
}
|
|
|
|
[[clang::coro_wrapper]] Co<int> plain_return_co(int b) {
|
|
return foo_coro(b); // expected-warning {{address of stack memory associated with parameter}}
|
|
}
|
|
|
|
[[clang::coro_wrapper]] Co<int> safe_forwarding(const int& b) {
|
|
return foo_coro(b);
|
|
}
|
|
|
|
[[clang::coro_wrapper]] Co<int> unsafe_wrapper(int b) {
|
|
return safe_forwarding(b); // expected-warning {{address of stack memory associated with parameter}}
|
|
}
|
|
|
|
[[clang::coro_wrapper]] Co<int> complex_plain_return(int b) {
|
|
return b > 0
|
|
? foo_coro(1) // expected-warning {{returning address of local temporary object}}
|
|
: bar_coro(0, 1); // expected-warning {{returning address of local temporary object}}
|
|
}
|
|
|
|
// =============================================================================
|
|
// Lambdas
|
|
// =============================================================================
|
|
namespace lambdas {
|
|
void lambdas() {
|
|
auto unsafe_lambda = [] [[clang::coro_wrapper]] (int b) {
|
|
return foo_coro(b); // expected-warning {{address of stack memory associated with parameter}}
|
|
};
|
|
auto coro_lambda = [] (const int&) -> Co<int> {
|
|
co_return 0;
|
|
};
|
|
auto unsafe_coro_lambda = [&] (const int& b) -> Co<int> {
|
|
int x = co_await coro_lambda(b);
|
|
auto safe = coro_lambda(b);
|
|
auto unsafe1 = coro_lambda(1); // expected-warning {{temporary whose address is used as value of local variable}}
|
|
auto unsafe2 = coro_lambda(getInt()); // expected-warning {{temporary whose address is used as value of local variable}}
|
|
auto unsafe3 = coro_lambda(co_await coro_lambda(b)); // expected-warning {{temporary whose address is used as value of local variable}}
|
|
co_return co_await safe;
|
|
};
|
|
auto safe_lambda = [](int b) -> Co<int> {
|
|
int x = co_await foo_coro(1);
|
|
co_return x + co_await foo_coro(b);
|
|
};
|
|
}
|
|
|
|
Co<int> lambda_captures() {
|
|
int a = 1;
|
|
// Temporary lambda object dies.
|
|
auto lamb = [a](int x, const int& y) -> Co<int> { // expected-warning {{temporary whose address is used as value of local variable 'lamb'}}
|
|
co_return x + y + a;
|
|
}(1, a);
|
|
// Object dies but it has no capture.
|
|
auto no_capture = []() -> Co<int> { co_return 1; }();
|
|
auto bad_no_capture = [](const int& a) -> Co<int> { co_return a; }(1); // expected-warning {{temporary}}
|
|
// Temporary lambda object with lifetime extension under co_await.
|
|
int res = co_await [a](int x, const int& y) -> Co<int> {
|
|
co_return x + y + a;
|
|
}(1, a);
|
|
// Lambda object on stack should be fine.
|
|
auto lamb2 = [a]() -> Co<int> { co_return a; };
|
|
auto on_stack = lamb2();
|
|
auto res2 = co_await on_stack;
|
|
co_return 1;
|
|
}
|
|
} // namespace lambdas
|
|
|
|
// =============================================================================
|
|
// Member coroutines
|
|
// =============================================================================
|
|
namespace member_coroutines{
|
|
struct S {
|
|
Co<int> member(const int& a) { co_return a; }
|
|
};
|
|
|
|
Co<int> use() {
|
|
S s;
|
|
int a = 1;
|
|
auto test1 = s.member(1); // expected-warning {{temporary whose address is used as value of local variable}}
|
|
auto test2 = s.member(a);
|
|
auto test3 = S{}.member(a); // expected-warning {{temporary whose address is used as value of local variable}}
|
|
co_return 1;
|
|
}
|
|
|
|
[[clang::coro_wrapper]] Co<int> wrapper(const int& a) {
|
|
S s;
|
|
return s.member(a); // expected-warning {{address of stack memory}}
|
|
}
|
|
} // member_coroutines
|
|
|
|
// =============================================================================
|
|
// Safe usage when parameters are value
|
|
// =============================================================================
|
|
namespace by_value {
|
|
Co<int> value_coro(int b) { co_return co_await foo_coro(b); }
|
|
[[clang::coro_wrapper]] Co<int> wrapper1(int b) { return value_coro(b); }
|
|
[[clang::coro_wrapper]] Co<int> wrapper2(const int& b) { return value_coro(b); }
|
|
} // namespace by_value
|
|
|
|
// =============================================================================
|
|
// Lifetime bound but not a Coroutine Return Type: No analysis.
|
|
// =============================================================================
|
|
namespace not_a_crt {
|
|
template <typename T> struct [[clang::coro_lifetimebound]] CoNoCRT {
|
|
struct promise_type {
|
|
CoNoCRT<T> get_return_object() {
|
|
return {};
|
|
}
|
|
suspend_always initial_suspend();
|
|
suspend_always final_suspend() noexcept;
|
|
void unhandled_exception();
|
|
void return_value(const T &t);
|
|
};
|
|
};
|
|
|
|
CoNoCRT<int> foo_coro(const int& a) { co_return a; }
|
|
CoNoCRT<int> bar(int a) {
|
|
auto x = foo_coro(a);
|
|
co_return 1;
|
|
}
|
|
} // namespace not_a_crt
|
|
|
|
// =============================================================================
|
|
// Not lifetime bound coroutine wrappers: [[clang::coro_disable_lifetimebound]].
|
|
// =============================================================================
|
|
namespace disable_lifetimebound {
|
|
Co<int> foo(int x) { co_return x; }
|
|
|
|
[[clang::coro_wrapper, clang::coro_disable_lifetimebound]]
|
|
Co<int> foo_wrapper(const int& x) { return foo(x); }
|
|
|
|
[[clang::coro_wrapper]] Co<int> caller() {
|
|
// The call to foo_wrapper is wrapper is safe.
|
|
return foo_wrapper(1);
|
|
}
|
|
|
|
struct S{
|
|
[[clang::coro_wrapper, clang::coro_disable_lifetimebound]]
|
|
Co<int> member(const int& x) { return foo(x); }
|
|
};
|
|
|
|
Co<int> use() {
|
|
S s;
|
|
int a = 1;
|
|
auto test1 = s.member(1); // param is not flagged.
|
|
auto test2 = S{}.member(a); // 'this' is not flagged.
|
|
co_return 1;
|
|
}
|
|
|
|
[[clang::coro_wrapper]] Co<int> return_stack_addr(const int& a) {
|
|
S s;
|
|
return s.member(a); // return of stack addr is not flagged.
|
|
}
|
|
} // namespace disable_lifetimebound
|