Hi Miao, and the Release Team, On Sat, Jan 03, 2026 at 12:53:30AM +0800, Miao Wang wrote: > Package: release.debian.org > Severity: normal > Tags: trixie > User: release.debian.org@packages.debian.org > Usertags: pu > Control: affects -1 + src:qtbase-opensource-src > X-Debbugs-Cc: qtbase-opensource-src@packages.debian.org, debian-qt-kde@lists.debian.org, Harry Chen <harry@debian.org> > > [ Reason ] > qtbase-opensource-src in trixie is affected by #1122641, where a data race in > QReadWriteLock on weakly ordering architectures is discovered. This data race > can cause qt3d-opensource-src FTBFS on such architectures, reporting > heavyDutyMultiThreadedAccess or heavyDutyMultiThreadedAccessRelease from the > test suite tst_qresourcemanager time out. > > [ Tests ] > I've tested the patches personally on arm64 and can confirm the introduced > patches can fix the issue in question. > > [ Risks ] > Risks are minimal. The changes in the patches are minimal enough, except the > added unit test. > > [ Checklist ] > [x] *all* changes are documented in the d/changelog > [x] I reviewed all changes and I approve them > [x] attach debdiff against the package in (old)stable > [x] the issue is verified as fixed in unstable > > [ Changes ] > * Non-maintainer upload. > * Backport two upstream patches to fix data races in QReadWriteLock > > [ Other info ] > n/a Please consider the attached debdiff instead. It also fixes bug #1107294: with some hardware configurations, there was division by zero in QXcbVirtualDesktop::dpi() function. To fix this, I backported upstream commit [1] which removes one call of that function and replaces it with a static value (fallback DPI = 96). That commit is part of Qt 6 since 2020. [1]: https://code.qt.io/cgit/qt/qtbase.git/commit?id=7238123521708ec9 -- Dmitry Shachnev
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,12 @@
+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).
+
+ -- Dmitry Shachnev <mitya57@debian.org> Fri, 02 Jan 2026 20:47:31 +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
--- a/debian/patches/series
+++ b/debian/patches/series
@@ -20,6 +20,9 @@ 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
# Debian specific.
no_htmlinfo_example.diff
Attachment:
signature.asc
Description: PGP signature