Conversation
There was a problem hiding this comment.
Pull request overview
Addresses #312 by changing the QFuture coroutine awaiter to ensure the internally-created QFutureWatcher is tied to an object lifetime owned by the awaiter, preventing it from leaking when the awaiting coroutine is destroyed early.
Changes:
- Parent the dynamically allocated
QFutureWatcher<T_>to an awaiter-ownedQObjectinstance. - Use a context-object
QObject::connect()overload so the finished handler is auto-disconnected when the awaiter is destroyed.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
qcoro/core/qcorofuture.h
Outdated
| auto *watcher = new QFutureWatcher<T_>(&dummy); | ||
| QObject::connect( | ||
| watcher, &QFutureWatcherBase::finished, | ||
| &dummy, [watcher, awaitingCoroutine]() mutable { | ||
| watcher->deleteLater(); | ||
| awaitingCoroutine.resume(); | ||
| }); |
There was a problem hiding this comment.
watcher is parented to dummy, but the finished handler calls awaitingCoroutine.resume() immediately. Resuming typically destroys the awaiter object before returning from resume(), which in turn destroys dummy and synchronously deletes its children. That means the QFutureWatcher can be deleted while it is in the middle of emitting finished, defeating the deleteLater() and risking re-entrancy/UAF in Qt signal delivery.
To keep the deletion deferred, either (a) detach the watcher from dummy before resuming (e.g., watcher->setParent(nullptr) and then deleteLater()), or (b) queue the coroutine resume (e.g., via QTimer::singleShot(0, ...) / queued invoke) so dummy destruction happens after the signal handler returns.
qcoro/core/qcorofuture.h
Outdated
| QObject dummy; | ||
| QFuture<T_> mFuture; |
There was a problem hiding this comment.
The new member dummy doesn’t follow the mX member naming convention already used in this class (mFuture) and its purpose isn’t obvious from the name. Consider renaming it to something descriptive like mWatcherParent/mContextObject (and keep the m prefix) to clarify that it exists purely for lifetime/connection management.
| void await_suspend(std::coroutine_handle<> awaitingCoroutine) { | ||
| auto *watcher = new QFutureWatcher<T_>(); | ||
| QObject::connect(watcher, &QFutureWatcherBase::finished, [watcher, awaitingCoroutine]() mutable { | ||
| watcher->deleteLater(); | ||
| awaitingCoroutine.resume(); | ||
| }); | ||
| auto *watcher = new QFutureWatcher<T_>(&dummy); | ||
| QObject::connect( | ||
| watcher, &QFutureWatcherBase::finished, | ||
| &dummy, [watcher, awaitingCoroutine]() mutable { | ||
| watcher->deleteLater(); | ||
| awaitingCoroutine.resume(); | ||
| }); | ||
| watcher->setFuture(mFuture); |
There was a problem hiding this comment.
This change is meant to fix the “awaiter destroyed before future finishes” case (leak/UAF). There are existing QFuture tests (e.g., tests/qfuture.cpp), but none appear to exercise destroying the awaiting Task before the QFuture completes. Adding a regression test that starts a long-running QFuture, suspends on co_await future, destroys the task, and then lets the future finish (asserting no crash) would help prevent regressions.
There was a problem hiding this comment.
@copilot open a new pull request to apply changes based on this feedback
9f7b26f to
f9284bb
Compare
|
@danvratil I've opened a new pull request, #317, to work on those changes. Once the pull request is ready, I'll request review from you. |
f9284bb to
a23a64f
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
qcoro/core/qcorofuture.h
Outdated
| explicit WaitForFinishedOperationBase(const QFuture<T_> &future) : mFuture(future) {} | ||
|
|
||
| Q_DISABLE_COPY(WaitForFinishedOperationBase) | ||
| QCORO_DEFAULT_MOVE(WaitForFinishedOperationBase) | ||
|
|
There was a problem hiding this comment.
WaitForFinishedOperationBase now contains a QObject member, which makes the explicitly defaulted move ctor/assignment (via QCORO_DEFAULT_MOVE) implicitly deleted. With the project’s -Werror builds, this commonly triggers -Wdefaulted-function-deleted and can fail compilation. Consider either removing QCORO_DEFAULT_MOVE here or making the context object movable (e.g., store it via std::unique_ptr / store the QFutureWatcher as an owning smart pointer member and use it as the connection context).
| private Q_SLOTS: | ||
| // Regression test for #312: verify that destroying a task while it is | ||
| // awaiting on a QFuture doesn't cause a crash or memory leak | ||
| void testTaskDestroyedBeforeFutureCompletes() { | ||
| QPromise<int> promise; | ||
| auto future = promise.future(); |
There was a problem hiding this comment.
This regression test is currently inside the #if QT_VERSION >= QT_VERSION_CHECK(6, 3, 1) block (added after testUnfinishedPromiseDestroyed_coro), but it doesn’t appear to depend on the Qt 6.3.1-specific QPromise cancellation behavior. Consider moving it outside that guard (e.g., under #if QT_VERSION_MAJOR >= 6) so it runs on all supported Qt 6 versions.
| { | ||
| auto task = [](QFuture<int> future) -> QCoro::Task<> { | ||
| co_await future; | ||
| }(future); |
There was a problem hiding this comment.
task is created but never used, which will typically trigger an unused-variable warning (and this repo enables -Werror for tests in Debug). Mark it [[maybe_unused]], use Q_UNUSED(task), or otherwise consume it (e.g., by moving it into a no-op sink) to keep builds warning-free.
| }(future); | |
| }(future); | |
| Q_UNUSED(task); |
a23a64f to
fbacc65
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
4718112 to
a05bcfb
Compare
a05bcfb to
fc27e2c
Compare
Fixes #312