Skip to content

Fix memory leak in QFuture coro wrapper#316

Merged
danvratil merged 1 commit intomainfrom
312-fix-qcorofuture-memory-leak
Feb 3, 2026
Merged

Fix memory leak in QFuture coro wrapper#316
danvratil merged 1 commit intomainfrom
312-fix-qcorofuture-memory-leak

Conversation

@danvratil
Copy link
Collaborator

Fixes #312

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-owned QObject instance.
  • 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.

Comment on lines +35 to +41
auto *watcher = new QFutureWatcher<T_>(&dummy);
QObject::connect(
watcher, &QFutureWatcherBase::finished,
&dummy, [watcher, awaitingCoroutine]() mutable {
watcher->deleteLater();
awaitingCoroutine.resume();
});
Copy link

Copilot AI Feb 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines 46 to 47
QObject dummy;
QFuture<T_> mFuture;
Copy link

Copilot AI Feb 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines 34 to 42
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);
Copy link

Copilot AI Feb 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot open a new pull request to apply changes based on this feedback

@danvratil danvratil force-pushed the 312-fix-qcorofuture-memory-leak branch from 9f7b26f to f9284bb Compare February 1, 2026 20:42
Copy link

Copilot AI commented Feb 1, 2026

@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.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines 25 to 29
explicit WaitForFinishedOperationBase(const QFuture<T_> &future) : mFuture(future) {}

Q_DISABLE_COPY(WaitForFinishedOperationBase)
QCORO_DEFAULT_MOVE(WaitForFinishedOperationBase)

Copy link

Copilot AI Feb 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Comment on lines +274 to +279
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();
Copy link

Copilot AI Feb 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
{
auto task = [](QFuture<int> future) -> QCoro::Task<> {
co_await future;
}(future);
Copy link

Copilot AI Feb 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
}(future);
}(future);
Q_UNUSED(task);

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@danvratil danvratil force-pushed the 312-fix-qcorofuture-memory-leak branch from 4718112 to a05bcfb Compare February 1, 2026 21:49
@danvratil danvratil force-pushed the 312-fix-qcorofuture-memory-leak branch from a05bcfb to fc27e2c Compare February 3, 2026 09:22
@danvratil danvratil merged commit f19a3a3 into main Feb 3, 2026
41 of 47 checks passed
@danvratil danvratil deleted the 312-fix-qcorofuture-memory-leak branch February 3, 2026 11:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

co_await future leaks watcher

3 participants