Uploaded image for project: 'Qt'
  1. Qt
  2. QTBUG-127880

Race condition in QFuture implementation

    XMLWordPrintable

Details

    • Linux/X11, macOS, Windows
    • 2bc82f6a1 (dev), 1873ff034 (6.8), f1b230b88 (6.7)
    • Foundation PM Prioritized

    Description

      TL;DR

      There is a race condition in function:

      [..\qtbase\src\corelib\thread\qfutureinterface.cpp]
      void QtPrivate::watchContinuationImpl(const QObject *context, QSlotObjectBase *slotObj, QFutureInterfaceBase &fi) 

      Context

      Since moving to Qt 6.7 our users started encountering crash with the following stack trace:

      Fatal Error: EXCEPTION_ACCESS_VIOLATION_READ / 0x20
      Qt6Core std::_Atomic_storage<T>::load (atomic:1134)
      Qt6Core QObjectPrivate::ConnectionData::cleanOrphanedConnections (qobject_p_p.h:172)
      Qt6Core doActivate<T> (qobject.cpp:4081)
      Qt6Core QMetaObject::activate (qobject.cpp:4099)
      Qt6Core QObjectContinuationWrapper::run (qfutureinterface.moc:159)
      Qt6Core QtPrivate::watchContinuationImpl::__l2::<lambda>::operator() (qfutureinterface.cpp:101)
      Qt6Core std::invoke (type_traits:1534)
      Qt6Core std::_Invoker_ret<T>::_Call (functional:651)
      Qt6Core std::_Func_impl_no_alloc<T>::_Do_call (functional:822)
      Qt6Core std::_Func_class<T>::operator() (functional:869)
      Qt6Core QFutureInterfaceBase::runContinuation (qfutureinterface.cpp:932)
      Qt6Core QThreadPoolThread::run (qthreadpool.cpp:66)
      Qt6Core QThreadPrivate::start (qthread_win.cpp:290)
      

      This is by far the most frequently occurring crash in our application. 

      Description

      Crash happens when Asynchronous task continuation is scheduled on the context thread.

      #include <QCoreApplication>
      #include <QtConcurrent>
      
      int main(int argc, char *argv[])
      {
          QCoreApplication a(argc, argv);
          
          auto context = new QObject;
      
          QtConcurrent::run([] {
              qDebug() << "Task on the pooled thread" << QThread::currentThreadId();
          }).then(context, [] {
              qDebug() << "Continuation on the context (main) thread" << QThread::currentThreadId();
          });
      
          return a.exec();
      } 

       

      Crash will happen rarely in production, but can be reliably reproduced by introducing artificial delays in Qt source code. Example output with artificial delay*:

      [doActivate<T>]            Delay on thread 0x298c
      Continuation on the context (main) thread 0x23e8
      [~QObjectContinuationWrapper<T>]        Deleting watcher on thread 0x23e8
      - Stopped in thread 4 by: Exception at 0x7ffc29b285ab, code: 0xc0000005: read access violation at: 0xffffffffffffffff, flags=0x0. 

      Analysis

       

      [..\qtbase\src\corelib\thread\qfutureinterface.cpp]
      (non-essential parts omitted for brevity, comments added)
      
      void QtPrivate::watchContinuationImpl(const QObject *context, QSlotObjectBase *slotObj, QFutureInterfaceBase &fi)
      {
          ...
      
          auto *watcher = new QObjectContinuationWrapper; // POI 1 This is on the main thread
          watcher->moveToThread(context->thread());
          
          ...
      
          QObject::connect(watcher, &QObjectContinuationWrapper::run,
                           context, [slot = std::move(slot)] {
                               void *args[] = { nullptr };
                               slot->call(nullptr, args);
                           }); // POI 3
      
          QObject::connect(watcher, &QObjectContinuationWrapper::run, watcher, &QObject::deleteLater); // POI 4
           
          ...
          
         fi.setContinuation([watcherMutex, watcher = QPointer(watcher)]
                             (const QFutureInterfaceBase &parentData)
          {
              Q_UNUSED(parentData);
              QMutexLocker lock(watcherMutex.get());
              if (watcher)
                  emit watcher->run(); // POI 2 This is from the pooled thread
          });
      } 

      Future signals  QObjectContinuationWrapper (POI 1 ) to run from the different thread. Because connected slot (POI 3 and 4) contexts are on the main thread, slots are asynchronously dispatched to the main thread (refer to doActivate<T> (qobject.cpp: 4042)). In rear occasions, deleteLater (POI 4) will be triggered first and {}watcher cleaned up in the event loop cycle before doActivate<T> (qobject.cpp from the pooled thread, POI 2){{ is finished.}}

       

      *Attached are QObject and QFutureIntefrace implementation files with minor modifications (artificial delay and debug output) to showcase the issue. Sources were taken from Qt 6.7.1

       

       
       

      Attachments

        1. main.cpp
          0.4 kB
          Vakhtang Khelashvili
        2. qfutureinterface.cpp
          29 kB
          Vakhtang Khelashvili
        3. qobject.cpp
          205 kB
          Vakhtang Khelashvili

        Issue Links

          For Gerrit Dashboard: QTBUG-127880
          # Subject Branch Project Status CR V

          Activity

            People

              msarehn Arno Rehn
              vk Vakhtang Khelashvili
              Vladimir Minenko Vladimir Minenko
              Alex Blasche Alex Blasche
              Votes:
              3 Vote for this issue
              Watchers:
              8 Start watching this issue

              Dates

                Created:
                Updated:
                Resolved:

                Gerrit Reviews

                  There are no open Gerrit changes