]> git.proxmox.com Git - ceph.git/blob - ceph/src/seastar/doc/lambda-coroutine-fiasco.md
update ceph source to reef 18.1.2
[ceph.git] / ceph / src / seastar / doc / lambda-coroutine-fiasco.md
1 # The Lambda Coroutine Fiasco
2
3 Lambda coroutines and Seastar APIs that accept continuations interact badly. This
4 document explain the bad interaction and how it is mitigated.
5
6 ## Lambda coroutines revisited
7
8 A lambda coroutine is a lambda function that is also a coroutine due
9 to the use of the coroutine keywords (typically co_await). A lambda
10 coroutine is notionally translated by the compiler into a struct with a
11 function call operator:
12
13 ```cpp
14 [captures] (arguments) -> seastar::future<> {
15 body
16 co_return result
17 }
18 ```
19
20 becomes (more or less)
21
22 ```cpp
23 struct lambda {
24 captures;
25 seastar::future<> operator()(arguments) const {
26 body
27 }
28 };
29 ```
30
31 ## Lambda coroutines and coroutine argument capture
32
33 In addition to a lambda capturing variables from its environment, the
34 coroutine also captures its arguments. This capture can happen by value
35 or reference, depending on how each argument is declared.
36
37 The lambda's captures however are captured by reference. To understand why,
38 consider that the coroutine translation process notionally transforms a member function
39 (`lambda::operator()`) to a free function:
40
41 ```cpp
42 // before
43 seastar::future<> lambda::operator()(arguments) const;
44
45 // after
46 seastar::future<> lambda_call_operator(const lambda& self, arguments);
47 ```
48
49 This transform means that the lambda structure, which contains all the captured variables,
50 is itself captured by the coroutine by reference.
51
52 ## Interaction with Seastar APIs accepting continuations
53
54 Consider a Seastar API that accepts a continuation, such as
55 `seastar::future::then(Func continuation)`. The behavior
56 is that `continuation` is moved or copied into a private memory
57 area managed by `then()`. Sometime later, the continuation is
58 executed (`Func::operator()`) and the memory area is freed.
59 Crucially, the memory area is freed as soon as `Func::operator()`
60 returns, which can be before the future returned by it becomes
61 ready. However, the coroutine can access the lambda captures
62 stored in this memory area after the future is returned and before
63 it becomes ready. This is a use-after-free.
64
65 ## Solution
66
67 The solution is to avoid copying or moving the lambda into
68 the memory area managed by `seastar::future::then()`. Instead,
69 the lambda spends its life as a temporary. We then rely on C++
70 temporary lifetime extension rules to extend its life until the
71 future returned is ready, at which point the captures can longer
72 be accessed.
73
74 ```cpp
75 co_await seastar::yield().then(seastar::coroutine::lambda([captures] () -> future<> {
76 co_await seastar::coroutine::maybe_yield();
77 // Can use `captures` here safely.
78 }));
79 ```
80
81 `seastar::coroutine::lambda` is very similar to `std::reference_wrapper` (the
82 only difference is that it works with temporaries); it can be safely moved to
83 the memory area managed by `seastar::future::then()` since it's only used
84 to call the real lambda, and then is safe to discard.
85
86 ## Alternative solution when lifetime extension cannot be used.
87
88 If the lambda coroutine is not co_await'ed immediately, we cannot rely on
89 lifetime extension and so we must name the coroutine and use `std::ref()` to
90 refer to it without copying it from the coroutine frame:
91
92 ```cpp
93 auto a_lambda = [captures] () -> future<> {
94 co_await seastar::coroutine::maybe_yield();
95 // Can use `captures` here safely.
96 };
97 auto f = seastar::yield().then(std::ref(a_lambda));
98 co_await std::move(f);
99 ```
100