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

Attachment: signature.asc
Description: PGP signature

Reply via email to