diff options
| author | Ivan Solovev <ivan.solovev@qt.io> | 2025-02-07 14:55:11 +0100 |
|---|---|---|
| committer | Ivan Solovev <ivan.solovev@qt.io> | 2025-02-11 12:19:40 +0100 |
| commit | 04b5d2b94f96c73a13973f6a57cefbf07d2e850b (patch) | |
| tree | 6e175be51cfe0c5e45d09e1a191de91b0393bb5f /src/corelib/thread/qfutureinterface.cpp | |
| parent | a8f55f4729918700b9597843c98f9f2505ff0d23 (diff) | |
QFuture: prevent the continuations from being executed twice
On some very rare cases the continuation could be executed twice in a
multithreaded scenario. Consider the following example:
auto f = QtConcurrent::run(someFunction);
f.then(someOtherFunction);
Assuming that QtConcurrent::run() is executed on Thread B, and the
continuation is attached in Thread A, the following call stack is
possible:
1. Thread B: someFunction() is finished
2. Thread B: QFutureInterface::reportFinished() is called
3. Thread B: QFutureInterfaceBase::reportFinished() is called and
finished.
4. Thread A: then() is called. It checks that the future is already
finished, so the attached continuation is executed
immediately.
5. Thread B: QFutureInterfaceBase::runContinuation() is executed.
At this point it sees that a continuation exists, and
executes it again.
Step 5 will lead to a crash, because we'll try to access a moved-from
QPromise.
To fix the issue, introduce a flag that is explicitly used to test if
the continuation was already executed or not.
The pre-existing code already uses a concept of a continuation state,
but it cannot be easily extended without convering it from an enum
to flags, because the canceled continuation should still be called
in order to actually execute the onCanceled() handler.
Wrapping QFIBP::ContinuationState into QFlags would increase its size
from 1 to 4 bytes, and manually treating the enum as flags will result
in a more complicated code. So, I simply picked the approach of adding
an explicit flag for this case.
Writing a unit-test is not really possible, but I verified that the
reproducer from the linked Jira ticket does not crash anymore.
Amends dfaca09e85a49d2983bb89893bfbe1ba4c19eab4 that introduced the
continuations, so picking to all active Qt 6 branches.
Fixes: QTBUG-118032
Pick-to: 6.9 6.8 6.5
Change-Id: I3e07722495e38367e8e4be2eded34140e22b0053
Reviewed-by: MÃ¥rten Nordheim <marten.nordheim@qt.io>
Reviewed-by: Fabian Kosmale <fabian.kosmale@qt.io>
Diffstat (limited to 'src/corelib/thread/qfutureinterface.cpp')
| -rw-r--r-- | src/corelib/thread/qfutureinterface.cpp | 4 |
1 files changed, 3 insertions, 1 deletions
diff --git a/src/corelib/thread/qfutureinterface.cpp b/src/corelib/thread/qfutureinterface.cpp index 7f1b2bef154..2236b5d1db1 100644 --- a/src/corelib/thread/qfutureinterface.cpp +++ b/src/corelib/thread/qfutureinterface.cpp @@ -888,6 +888,7 @@ void QFutureInterfaceBase::setContinuation(std::function<void(const QFutureInter // If the state is ready, run continuation immediately, // otherwise save it for later. if (isFinished()) { + d->continuationExecuted = true; lock.unlock(); func(*this); lock.relock(); @@ -919,10 +920,11 @@ void QFutureInterfaceBase::cleanContinuation() void QFutureInterfaceBase::runContinuation() const { QMutexLocker lock(&d->continuationMutex); - if (d->continuation) { + if (d->continuation && !d->continuationExecuted) { // Save the continuation in a local function, to avoid calling // a null std::function below, in case cleanContinuation() is // called from some other thread right after unlock() below. + d->continuationExecuted = true; auto fn = std::move(d->continuation); lock.unlock(); fn(*this); |
