Skip to content

Commit e404eda

Browse files
committed
DeferredProxyCoroutine: Implement a guarantee against incorrect destruction order
1 parent 6bf1729 commit e404eda

File tree

2 files changed

+68
-3
lines changed

2 files changed

+68
-3
lines changed

src/workerd/api/deferred-proxy-test.c++

+42
Original file line numberDiff line numberDiff line change
@@ -229,5 +229,47 @@ KJ_TEST("kj::Promise<DeferredProxy<T>>: can be canceled while suspended after de
229229
KJ_EXPECT(unwind == 4);
230230
}
231231

232+
KJ_TEST("kj::Promise<DeferredProxy<T>>: destroying inner PromiseNode before outer does not "
233+
"segfault") {
234+
// Destroy the inner promise before the outer promise to test our safeguard against incorrect
235+
// destruction order causing segfaults.
236+
237+
kj::EventLoop loop;
238+
kj::WaitScope waitScope(loop);
239+
240+
auto coro = []() -> kj::Promise<DeferredProxy<void>> {
241+
KJ_CO_MAGIC BEGIN_DEFERRED_PROXYING;
242+
co_await kj::Promise<void>(kj::NEVER_DONE);
243+
};
244+
245+
auto outer = coro();
246+
247+
// We could call `get()` on the outer node immediately, even before it reports it is ready, but
248+
// we call `poll()` for good measure, in case the DeferredProxyCoroutine implementation ever
249+
// changes to disallow `get()`-before-ready. We cannot use `wait()` for this purpose, because
250+
// `wait()` would avoid the segfault by (correctly) destroying the outer PromiseNode before
251+
// returning the result to us.
252+
KJ_EXPECT(outer.poll(waitScope));
253+
254+
auto outerNode = kj::_::PromiseNode::from(kj::mv(outer));
255+
256+
// `poll()`, unlike `wait()`, does not call `setSelfPointer()` on the outer PromiseNode, which
257+
// would cause an assertion failure inside the outer PromiseNode's `get()` implementation, so we
258+
// have to do it ourselves.
259+
outerNode->setSelfPointer(&outerNode);
260+
261+
kj::_::ExceptionOr<DeferredProxy<void>> result;
262+
outerNode->get(result);
263+
264+
{
265+
// Destroy the inner promise.
266+
auto inner = kj::mv(KJ_ASSERT_NONNULL(result.value).proxyTask);
267+
}
268+
269+
// Destroy the outer promise. At one time, this caused a segfault ... or at least it produced
270+
// invalid accesses under valgrind. :/
271+
outerNode = nullptr;
272+
}
273+
232274
} // namespace
233275
} // namespace workerd::api

src/workerd/api/deferred-proxy.h

+26-3
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
#pragma once
66

77
#include <kj/async.h>
8+
#include <kj/debug.h>
89

910
namespace workerd::api {
1011

@@ -211,14 +212,19 @@ class DeferredProxyCoroutine: public kj::_::PromiseNode,
211212

212213
// PromiseNode implementation
213214

215+
void setSelfPointer(kj::_::OwnPromiseNode* selfPtr) noexcept override {
216+
this->selfPtr = selfPtr;
217+
}
218+
214219
void destroy() override {
215220
// The promise returned by `inner.get_return_object()` is what actually owns this coroutine
216221
// frame. We temporarily store that in `result` until our outer promise is fulfilled. So, to
217222
// destroy ourselves, we must manually drop `result`.
218223
//
219-
// On the other hand, if our outer promise has already been fulfilled, and `result` delivered to
220-
// wherever it is going, then someone else directly owns the coroutine now, not us, and it's
221-
// okay for this drop to be a no-op.
224+
// On the other hand, if our outer promise has already been fulfilled, then `result` has already
225+
// been delivered to wherever it is going, and someone else directly owns the coroutine now, not
226+
// us. In this case, this `destroy()` override will have already been called (and it will have
227+
// been a no-op), because our own OwnPromiseNode will have already been dropped in `get()`.
222228

223229
auto drop = kj::mv(result);
224230
}
@@ -228,6 +234,13 @@ class DeferredProxyCoroutine: public kj::_::PromiseNode,
228234
}
229235

230236
void get(kj::_::ExceptionOrValue& output) noexcept override {
237+
// Make sure that the outer PromiseNode (`this` one) is destroyed before the inner PromiseNode.
238+
// kj-async should already provide us this guarantee, but since incorrect destruction order
239+
// would cause invalid memory access, we provide a stronger guarantee. Also see the comment for
240+
// the `result` data member.
241+
KJ_ASSERT(selfPtr != nullptr);
242+
KJ_DEFER(*selfPtr = nullptr);
243+
231244
static_cast<decltype(result)&>(output) = kj::mv(result);
232245
}
233246

@@ -249,6 +262,16 @@ class DeferredProxyCoroutine: public kj::_::PromiseNode,
249262

250263
kj::_::ExceptionOr<DeferredProxy<T>> result;
251264
// Stores the result for the outer promise.
265+
//
266+
// WARNING: This object owns `this` PromiseNode! If `result` is ever moved away, as is done in
267+
// `get()`, we must arrange to make sure that no one ever tries to use `this` PromiseNode again.
268+
// Stated another way, we must guarantee that the outer PromiseNode (for `DeferredProxy<T>`) is
269+
// always destroyed before the inner PromiseNode (for `T`). kj-async always does this anyway, but
270+
// we implement an additional safeguard by immediately destroying our own `OwnPromiseNode` (which
271+
// we have access to via `setSelfPointer()`) when we move `result` away in `get()`.
272+
273+
kj::_::OwnPromiseNode* selfPtr = nullptr;
274+
// Used to drop ourselves in `get()` -- see comment for `result`.
252275

253276
bool deferredProxyingHasBegun = false;
254277
// Set to true when deferred proxying has begun -- that is, when the outer DeferredProxy<T>

0 commit comments

Comments
 (0)