Hi Miao, and the Release team, On Thu, Jan 22, 2026 at 03:14:39PM +0300, Dmitry Shachnev wrote: > On Thu, Jan 22, 2026 at 08:05:39PM +0800, Miao Wang wrote: > > I wonder if we can wait a couple of days for upstream > > merge the fix for #1126100 and integrate it into this > > stable-pu release? > > Sure, we can wait. We have not yet got a green light from the release team > anyway.
Attached a new debdiff with added fix for #1126100. This patch reverts a commit from 2019 (first included Qt 5.14) that attempted to simplify locking; that simplification was wrong and caused a data race. So the new debdiff has 4 patches in total: - Three patches for various data race fixes (#1122641, #1126100). - One patch to prevent division by zero in QXcbScreen (#1107294). All these bugs are fixed in sid. -- Dmitry Shachnev
--- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,14 @@ +qtbase-opensource-src (5.15.15+dfsg-6+deb13u1) trixie; urgency=medium + + * Backport two upstream patches to fix data races in QReadWriteLock + (closes: #1122641). + * Backport upstream patch to stop calling QXcbVirtualDesktop::dpi() + function from QXcbScreen::logicalDpi() (closes: #1107294). + * Backport upstream patch to revert locking simplification, which caused + data race (closes: #1126100). + + -- Dmitry Shachnev <[email protected]> Sat, 31 Jan 2026 12:35:29 +0300 + qtbase-opensource-src (5.15.15+dfsg-6) unstable; urgency=medium * Backport upstream patch to fix assertion errors in data: URL parsing --- /dev/null +++ b/debian/patches/dont_use_physical_dpi.diff @@ -0,0 +1,37 @@ +Description: X11: set fallback logical DPI to 96 + Returning physical DPI from logicalDpi() is problematic, + as explained in commit 77e04acb. + . + The most predictable implementation is to never return + physical DPI from QPlaformScreen::logicalDpi(). Other + platform plugins already do this, and this change + brings xcb in line with the rest of Qt. + . + We have the QPlatformScreen::physicalSize() API which + covers returning physical DPI (indirectly); Options + for selecting which one to use can be implemented on + top of these (see QT_USE_PHYSICAL_DPI). +Origin: upstream, https://code.qt.io/cgit/qt/qtbase.git/commit?id=7238123521708ec9 +Last-Update: 2025-12-31 + +--- a/src/plugins/platforms/xcb/qxcbscreen.cpp ++++ b/src/plugins/platforms/xcb/qxcbscreen.cpp +@@ -731,12 +731,12 @@ QDpi QXcbScreen::logicalDpi() const + if (forcedDpi > 0) + return QDpi(forcedDpi, forcedDpi); + +- // Fall back to physical virtual desktop DPI, but prevent +- // using DPI values lower than 96. This ensuers that connecting +- // to e.g. a TV works somewhat predictabilly. +- QDpi virtualDesktopPhysicalDPi = m_virtualDesktop->dpi(); +- return QDpi(std::max(virtualDesktopPhysicalDPi.first, 96.0), +- std::max(virtualDesktopPhysicalDPi.second, 96.0)); ++ // Fall back to 96 DPI in case no logical DPI is set. We don't want to ++ // return physical DPI here, since that is a different type of DPI: Logical ++ // DPI typically accounts for user preference and viewing distance, and is ++ // quantized into DPI classes (96, 144, 192, etc); physical DPI is an exact ++ // physical measure. ++ return QDpi(96, 96); + } + + QPlatformCursor *QXcbScreen::cursor() const --- /dev/null +++ b/debian/patches/qreadwritelock_data_race.diff @@ -0,0 +1,33 @@ +Description: QReadWriteLock: fix data race on the d_ptr members + The loadRelaxed() at the beginning of tryLockForRead/tryLockForWrite + isn't enough to bring us the non-atomic write of the recursive bool. + Same issue with the std::mutex itself. +Origin: upstream, https://code.qt.io/cgit/qt/qtbase.git/commit?id=80d01c4ccb697b9d +Last-Update: 2025-12-14 + +--- a/src/corelib/thread/qreadwritelock.cpp ++++ b/src/corelib/thread/qreadwritelock.cpp +@@ -258,7 +258,10 @@ bool QReadWriteLock::tryLockForRead(int + d = val; + } + Q_ASSERT(!isUncontendedLocked(d)); +- // d is an actual pointer; ++ // d is an actual pointer; acquire its contents ++ d = d_ptr.loadAcquire(); ++ if (!d || isUncontendedLocked(d)) ++ continue; + + if (d->recursive) + return d->recursiveLockForRead(timeout); +@@ -365,7 +368,10 @@ bool QReadWriteLock::tryLockForWrite(int + d = val; + } + Q_ASSERT(!isUncontendedLocked(d)); +- // d is an actual pointer; ++ // d is an actual pointer; acquire its contents ++ d = d_ptr.loadAcquire(); ++ if (!d || isUncontendedLocked(d)) ++ continue; + + if (d->recursive) + return d->recursiveLockForWrite(timeout); --- /dev/null +++ b/debian/patches/qreadwritelock_data_race_2.diff @@ -0,0 +1,163 @@ +Description: QReadWriteLock: fix data race on weakly-ordered memory architectures + The fix changes the relaxed load of d_ptr in lockFor{Read,Write} after + the acquire of the mutex to an acquire load, to establish + synchronization with the release store of d_ptr when converting from an + uncontended lock to a contended lock. +Origin: upstream, https://code.qt.io/cgit/qt/qtbase.git/commit?id=4fd88011fa7975ce +Last-Update: 2025-12-14 + +--- a/src/corelib/thread/qreadwritelock.cpp ++++ b/src/corelib/thread/qreadwritelock.cpp +@@ -267,14 +267,14 @@ bool QReadWriteLock::tryLockForRead(int + return d->recursiveLockForRead(timeout); + + auto lock = qt_unique_lock(d->mutex); +- if (d != d_ptr.loadRelaxed()) { ++ if (QReadWriteLockPrivate *dd = d_ptr.loadAcquire(); d != dd) { + // d_ptr has changed: this QReadWriteLock was unlocked before we had + // time to lock d->mutex. + // We are holding a lock to a mutex within a QReadWriteLockPrivate + // that is already released (or even is already re-used). That's ok + // because the QFreeList never frees them. + // Just unlock d->mutex (at the end of the scope) and retry. +- d = d_ptr.loadAcquire(); ++ d = dd; + continue; + } + return d->lockForRead(timeout); +@@ -377,11 +377,11 @@ bool QReadWriteLock::tryLockForWrite(int + return d->recursiveLockForWrite(timeout); + + auto lock = qt_unique_lock(d->mutex); +- if (d != d_ptr.loadRelaxed()) { ++ if (QReadWriteLockPrivate *dd = d_ptr.loadAcquire(); d != dd) { + // The mutex was unlocked before we had time to lock the mutex. + // We are holding to a mutex within a QReadWriteLockPrivate that is already released + // (or even is already re-used) but that's ok because the QFreeList never frees them. +- d = d_ptr.loadAcquire(); ++ d = dd; + continue; + } + return d->lockForWrite(timeout); +--- a/tests/auto/corelib/thread/qreadwritelock/tst_qreadwritelock.cpp ++++ b/tests/auto/corelib/thread/qreadwritelock/tst_qreadwritelock.cpp +@@ -85,6 +85,7 @@ private slots: + void multipleReadersLoop(); + void multipleWritersLoop(); + void multipleReadersWritersLoop(); ++ void heavyLoadLocks(); + void countingTest(); + void limitedReaders(); + void deleteOnUnlock(); +@@ -635,6 +636,111 @@ public: + } + }; + ++class HeavyLoadLockThread : public QThread ++{ ++public: ++ QReadWriteLock &testRwlock; ++ const qsizetype iterations; ++ const int numThreads; ++ inline HeavyLoadLockThread(QReadWriteLock &l, qsizetype iters, int numThreads, QVector<QAtomicInt *> &counters): ++ testRwlock(l), ++ iterations(iters), ++ numThreads(numThreads), ++ counters(counters) ++ { } ++ ++private: ++ QVector<QAtomicInt *> &counters; ++ QAtomicInt *getCounter(qsizetype index) ++ { ++ QReadLocker locker(&testRwlock); ++ /* ++ The index is increased monotonically, so the index ++ being requested should be always within or at the end of the ++ counters vector. ++ */ ++ Q_ASSERT(index <= counters.size()); ++ if (counters.size() <= index || counters[index] == nullptr) { ++ locker.unlock(); ++ QWriteLocker wlocker(&testRwlock); ++ if (counters.size() <= index) ++ counters.resize(index + 1, nullptr); ++ if (counters[index] == nullptr) ++ counters[index] = new QAtomicInt(0); ++ return counters[index]; ++ } ++ return counters[index]; ++ } ++ void releaseCounter(qsizetype index) ++ { ++ QWriteLocker locker(&testRwlock); ++ delete counters[index]; ++ counters[index] = nullptr; ++ } ++ ++public: ++ void run() override ++ { ++ for (qsizetype i = 0; i < iterations; ++i) { ++ QAtomicInt *counter = getCounter(i); ++ /* ++ Here each counter is accessed by each thread ++ and increaed only once. As a result, when the ++ counter reaches numThreads, i.e. the fetched ++ value before the increment is numThreads-1, ++ we know all threads have accessed this counter ++ and we can delete it safely. ++ */ ++ int prev = counter->fetchAndAddRelaxed(1); ++ if (prev == numThreads - 1) { ++#ifdef QT_BUILDING_UNDER_TSAN ++ /* ++ Under TSAN, deleting and freeing an object ++ will trigger a write operation on the memory ++ of the object. Since we used fetchAndAddRelaxed ++ to update the counter, TSAN will report a data ++ race when deleting the counter here. To avoid ++ the false positive, we simply reset the counter ++ to 0 here, with ordered semantics to establish ++ the sequence to ensure the the free-ing option ++ happens after all fetchAndAddRelaxed operations ++ in other threads. ++ ++ When not building under TSAN, deleting the counter ++ will not result in any data read or written to the ++ memory region of the counter, so no data race will ++ happen. ++ */ ++ counter->fetchAndStoreOrdered(0); ++#endif ++ releaseCounter(i); ++ } ++ } ++ } ++}; ++ ++/* ++ Multiple threads racing acquiring and releasing ++ locks on the same indices. ++*/ ++ ++void tst_QReadWriteLock::heavyLoadLocks() ++{ ++ constexpr qsizetype iterations = 65536 * 4; ++ constexpr int numThreads = 8; ++ QVector<QAtomicInt *> counters; ++ QReadWriteLock testLock; ++ std::array<std::unique_ptr<HeavyLoadLockThread>, numThreads> threads; ++ for (auto &thread : threads) ++ thread = std::make_unique<HeavyLoadLockThread>(testLock, iterations, numThreads, counters); ++ for (auto &thread : threads) ++ thread->start(); ++ for (auto &thread : threads) ++ thread->wait(); ++ QVERIFY(counters.size() == iterations); ++ for (qsizetype i = 0; i < iterations; ++i) ++ QVERIFY(counters[i] == nullptr); ++} + + /* + A writer acquires a read-lock, a reader locks --- /dev/null +++ b/debian/patches/revert_simplify_locking.diff @@ -0,0 +1,183 @@ +Description: revert "QProcessEnvironment: simplify locking" + This reverts commit c5d6b263c204cb09db2be36826e19acb03dc24fb. + . + The commit being reverted assumes the mutex is only protecting 'nameMap' + and nothing else is mutable, which is false. The mutex is not only + protecting 'nameMap' but also protecting the containing value objects, + since even though the value object is accessed read-only, its + implementation mutates its internal states for 2-way conversion between + ByteArray and QString. + . + Commit 85e61297f7b02297641826332dbdbc845a88c34b ("restore + QProcessEnvironment shared data thread safety on unix") said that + implicit sharing together with 'mutable' is a time bomb and the bomb is + triggered by the reverted commit. +Origin: upstream, https://code.qt.io/cgit/qt/qtbase.git/commit?id=080d61c020678b75 +Last-Update: 2026-01-29 + +--- a/src/corelib/io/qprocess.cpp ++++ b/src/corelib/io/qprocess.cpp +@@ -202,7 +202,6 @@ void QProcessEnvironmentPrivate::insert( + vars.insert(it.key(), it.value()); + + #ifdef Q_OS_UNIX +- const OrderedNameMapMutexLocker locker(this, &other); + auto nit = other.nameMap.constBegin(); + const auto nend = other.nameMap.constEnd(); + for ( ; nit != nend; ++nit) +@@ -276,6 +275,7 @@ bool QProcessEnvironment::operator==(con + return true; + if (d) { + if (other.d) { ++ QProcessEnvironmentPrivate::OrderedMutexLocker locker(d, other.d); + return d->vars == other.d->vars; + } else { + return isEmpty(); +@@ -322,6 +322,7 @@ bool QProcessEnvironment::contains(const + { + if (!d) + return false; ++ QProcessEnvironmentPrivate::MutexLocker locker(d); + return d->vars.contains(d->prepareName(name)); + } + +@@ -372,6 +373,7 @@ QString QProcessEnvironment::value(const + if (!d) + return defaultValue; + ++ QProcessEnvironmentPrivate::MutexLocker locker(d); + const auto it = d->vars.constFind(d->prepareName(name)); + if (it == d->vars.constEnd()) + return defaultValue; +@@ -396,6 +398,7 @@ QStringList QProcessEnvironment::toStrin + { + if (!d) + return QStringList(); ++ QProcessEnvironmentPrivate::MutexLocker locker(d); + return d->toList(); + } + +@@ -409,6 +412,7 @@ QStringList QProcessEnvironment::keys() + { + if (!d) + return QStringList(); ++ QProcessEnvironmentPrivate::MutexLocker locker(d); + return d->keys(); + } + +@@ -425,6 +429,7 @@ void QProcessEnvironment::insert(const Q + return; + + // our re-impl of detach() detaches from null ++ QProcessEnvironmentPrivate::MutexLocker locker(e.d); + d->insert(*e.d); + } + +--- a/src/corelib/io/qprocess_p.h ++++ b/src/corelib/io/qprocess_p.h +@@ -146,22 +146,16 @@ public: + inline QString nameToString(const Key &name) const { return name; } + inline Value prepareValue(const QString &value) const { return value; } + inline QString valueToString(const Value &value) const { return value; } +-#else +- struct NameMapMutexLocker : public QMutexLocker +- { +- NameMapMutexLocker(const QProcessEnvironmentPrivate *d) : QMutexLocker(&d->nameMapMutex) {} ++ struct MutexLocker { ++ MutexLocker(const QProcessEnvironmentPrivate *) {} + }; +- struct OrderedNameMapMutexLocker : public QOrderedMutexLocker +- { +- OrderedNameMapMutexLocker(const QProcessEnvironmentPrivate *d1, +- const QProcessEnvironmentPrivate *d2) +- : QOrderedMutexLocker(&d1->nameMapMutex, &d2->nameMapMutex) +- {} ++ struct OrderedMutexLocker { ++ OrderedMutexLocker(const QProcessEnvironmentPrivate *, ++ const QProcessEnvironmentPrivate *) {} + }; +- ++#else + inline Key prepareName(const QString &name) const + { +- const NameMapMutexLocker locker(this); + Key &ent = nameMap[name]; + if (ent.isEmpty()) + ent = name.toLocal8Bit(); +@@ -170,27 +164,40 @@ public: + inline QString nameToString(const Key &name) const + { + const QString sname = QString::fromLocal8Bit(name); +- { +- const NameMapMutexLocker locker(this); +- nameMap[sname] = name; +- } ++ nameMap[sname] = name; + return sname; + } + inline Value prepareValue(const QString &value) const { return Value(value); } + inline QString valueToString(const Value &value) const { return value.string(); } + ++ struct MutexLocker : public QMutexLocker ++ { ++ MutexLocker(const QProcessEnvironmentPrivate *d) : QMutexLocker(&d->mutex) {} ++ }; ++ struct OrderedMutexLocker : public QOrderedMutexLocker ++ { ++ OrderedMutexLocker(const QProcessEnvironmentPrivate *d1, ++ const QProcessEnvironmentPrivate *d2) : ++ QOrderedMutexLocker(&d1->mutex, &d2->mutex) ++ {} ++ }; ++ + QProcessEnvironmentPrivate() : QSharedData() {} + QProcessEnvironmentPrivate(const QProcessEnvironmentPrivate &other) : +- QSharedData(), vars(other.vars) ++ QSharedData() + { ++ // This being locked ensures that the functions that only assign ++ // d pointers don't need explicit locking. + // We don't need to lock our own mutex, as this object is new and + // consequently not shared. For the same reason, non-const methods + // do not need a lock, as they detach objects (however, we need to + // ensure that they really detach before using prepareName()). +- NameMapMutexLocker locker(&other); ++ MutexLocker locker(&other); ++ vars = other.vars; + nameMap = other.nameMap; +- // We need to detach our nameMap, so that our mutex can protect it. +- // As we are being detached, it likely would be detached a moment later anyway. ++ // We need to detach our members, so that our mutex can protect them. ++ // As we are being detached, they likely would be detached a moment later anyway. ++ vars.detach(); + nameMap.detach(); + } + #endif +@@ -201,7 +208,8 @@ public: + #ifdef Q_OS_UNIX + typedef QHash<QString, Key> NameHash; + mutable NameHash nameMap; +- mutable QMutex nameMapMutex; ++ ++ mutable QMutex mutex; + #endif + + static QProcessEnvironment fromList(const QStringList &list); +--- a/src/corelib/io/qprocess_unix.cpp ++++ b/src/corelib/io/qprocess_unix.cpp +@@ -439,6 +439,7 @@ void QProcessPrivate::startProcess() + int envc = 0; + char **envp = nullptr; + if (environment.d.constData()) { ++ QProcessEnvironmentPrivate::MutexLocker locker(environment.d); + envp = _q_dupEnvironment(environment.d.constData()->vars, &envc); + } + +@@ -980,6 +981,7 @@ bool QProcessPrivate::startDetached(qint + int envc = 0; + char **envp = nullptr; + if (environment.d.constData()) { ++ QProcessEnvironmentPrivate::MutexLocker locker(environment.d); + envp = _q_dupEnvironment(environment.d.constData()->vars, &envc); + } + --- a/debian/patches/series +++ b/debian/patches/series @@ -20,6 +20,10 @@ dont_fallback_to_x11_tray_on_non_x11.diff check_dbus_tray_availability_every_time.diff a11y_null_checks.diff CVE-2025-5455.diff +qreadwritelock_data_race.diff +qreadwritelock_data_race_2.diff +dont_use_physical_dpi.diff +revert_simplify_locking.diff # Debian specific. no_htmlinfo_example.diff
signature.asc
Description: PGP signature

