Package: release.debian.org
Severity: normal
Tags: bookworm
X-Debbugs-Cc: qtbase-opensource-...@packages.debian.org
Control: affects -1 + src:qtbase-opensource-src
User: release.debian....@packages.debian.org
Usertags: pu

Dear Release team,

I would like to request a stable update of qtbase package for Bookworm.

[ Reason ]
It will fix two bugs:
- #1081682: Segfault in QAccessibleTableInterface::cellAt
- #1076293: CVE-2024-39936

[ Impact ]
The first bug is an important issue for screen reader users, which
makes Qt applications with tables unusable.

The second bug is minor security issue.
See https://security-tracker.debian.org/tracker/CVE-2024-39936 for details.

[ Tests ]
The reporter of #1081682 has tested the patch and it works for them.
See https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1081682#17.

The patch for CVE-2024-39936 was not tested explicitly, but it was applied
in sid since 2024-07-14 and no issues were reported.

[ Risks ]
The patch for #1081682 is quite trivial, it just adds NULL pointer checks.

The patch for CVE-2024-39936 is a bit more complex, but it has a verbose
explanation of what it does (I have included it in the Debian patch too).

[ 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 ]
  * Backport upstream patch to add null checks in table iface methods in
    linuxaccessibility/atspiadaptor.cpp (closes: #1081682).
  * Backport upstream patch to delay any communication until encrypted() can
    be responded to (CVE-2024-39936, closes: #1076293).

[ Other info ]
Links to upstream bugs, commits, patches for the first issue:

- https://bugreports.qt.io/browse/QTBUG-125954
- https://codereview.qt-project.org/c/qt/qtbase/+/518991
- https://invent.kde.org/qt/qt/qtbase/-/commit/076da096464a5d3f (KDE branch)

For the second issue:

- https://codereview.qt-project.org/c/qt/qtbase/+/571601
- 
https://download.qt.io/official_releases/qt/5.15/CVE-2024-39936-qtbase-5.15.patch
- https://invent.kde.org/qt/qt/qtbase/-/commit/0581ace6d4b8c0c0 (KDE branch)

--
Dmitry Shachnev
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,12 @@
+qtbase-opensource-src (5.15.8+dfsg-11+deb12u3) bookworm; urgency=medium
+
+  * Backport upstream patch to add null checks in table iface methods in
+    linuxaccessibility/atspiadaptor.cpp (closes: #1081682).
+  * Backport upstream patch to delay any communication until encrypted() can
+    be responded to (CVE-2024-39936, closes: #1076293).
+
+ -- Dmitry Shachnev <mity...@debian.org>  Mon, 24 Mar 2025 11:41:15 +0300
+
 qtbase-opensource-src (5.15.8+dfsg-11+deb12u2) bookworm; urgency=medium
 
   * Non-maintainer upload by the LTS Team.
--- /dev/null
+++ b/debian/patches/CVE-2024-39936.diff
@@ -0,0 +1,153 @@
+Description: HTTP2: delay any communication until encrypted() can be responded to
+ We have the encrypted() signal that lets users do extra checks on the
+ established connection. It is emitted as BlockingQueued, so the HTTP
+ thread stalls until it is done emitting. Users can potentially call
+ abort() on the QNetworkReply at that point, which is passed as a Queued
+ call back to the HTTP thread. That means that any currently queued
+ signal emission will be processed before the abort() call is processed.
+ .
+ In the case of HTTP2 it is a little special since it is multiplexed and
+ the code is built to start requests as they are available. This means
+ that, while the code worked fine for HTTP1, since one connection only
+ has one request, it is not working for HTTP2, since we try to send more
+ requests in-between the encrypted() signal and the abort() call.
+ .
+ This patch changes the code to delay any communication until the
+ encrypted() signal has been emitted and processed, for HTTP2 only.
+ It's done by adding a few booleans, both to know that we have to return
+ early and so we can keep track of what events arose and what we need to
+ resume once enough time has passed that any abort() call must have been
+ processed.
+Origin: upstream, https://code.qt.io/cgit/qt/qtbase.git/commit/?id=b1e75376cc3adfc7
+Last-Update: 2025-03-25
+
+--- a/src/network/access/qhttp2protocolhandler.cpp
++++ b/src/network/access/qhttp2protocolhandler.cpp
+@@ -371,12 +371,12 @@ bool QHttp2ProtocolHandler::sendRequest(
+         }
+     }
+ 
+-    if (!prefaceSent && !sendClientPreface())
+-        return false;
+-
+     if (!requests.size())
+         return true;
+ 
++    if (!prefaceSent && !sendClientPreface())
++        return false;
++
+     m_channel->state = QHttpNetworkConnectionChannel::WritingState;
+     // Check what was promised/pushed, maybe we do not have to send a request
+     // and have a response already?
+--- a/src/network/access/qhttpnetworkconnectionchannel.cpp
++++ b/src/network/access/qhttpnetworkconnectionchannel.cpp
+@@ -255,6 +255,10 @@ void QHttpNetworkConnectionChannel::abor
+ bool QHttpNetworkConnectionChannel::sendRequest()
+ {
+     Q_ASSERT(!protocolHandler.isNull());
++    if (waitingForPotentialAbort) {
++        needInvokeSendRequest = true;
++        return false; // this return value is unused
++    }
+     return protocolHandler->sendRequest();
+ }
+ 
+@@ -267,21 +271,28 @@ bool QHttpNetworkConnectionChannel::send
+ void QHttpNetworkConnectionChannel::sendRequestDelayed()
+ {
+     QMetaObject::invokeMethod(this, [this] {
+-        Q_ASSERT(!protocolHandler.isNull());
+         if (reply)
+-            protocolHandler->sendRequest();
++            sendRequest();
+     }, Qt::ConnectionType::QueuedConnection);
+ }
+ 
+ void QHttpNetworkConnectionChannel::_q_receiveReply()
+ {
+     Q_ASSERT(!protocolHandler.isNull());
++    if (waitingForPotentialAbort) {
++        needInvokeReceiveReply = true;
++        return;
++    }
+     protocolHandler->_q_receiveReply();
+ }
+ 
+ void QHttpNetworkConnectionChannel::_q_readyRead()
+ {
+     Q_ASSERT(!protocolHandler.isNull());
++    if (waitingForPotentialAbort) {
++        needInvokeReadyRead = true;
++        return;
++    }
+     protocolHandler->_q_readyRead();
+ }
+ 
+@@ -1289,7 +1300,18 @@ void QHttpNetworkConnectionChannel::_q_e
+             // Similar to HTTP/1.1 counterpart below:
+             const auto &pairs = spdyRequestsToSend.values(); // (request, reply)
+             const auto &pair = pairs.first();
++            waitingForPotentialAbort = true;
+             emit pair.second->encrypted();
++
++            // We don't send or handle any received data until any effects from
++            // emitting encrypted() have been processed. This is necessary
++            // because the user may have called abort(). We may also abort the
++            // whole connection if the request has been aborted and there is
++            // no more requests to send.
++            QMetaObject::invokeMethod(this,
++                                      &QHttpNetworkConnectionChannel::checkAndResumeCommunication,
++                                      Qt::QueuedConnection);
++
+             // In case our peer has sent us its settings (window size, max concurrent streams etc.)
+             // let's give _q_receiveReply a chance to read them first ('invokeMethod', QueuedConnection).
+             QMetaObject::invokeMethod(connection, "_q_startNextRequest", Qt::QueuedConnection);
+@@ -1307,6 +1329,26 @@ void QHttpNetworkConnectionChannel::_q_e
+     }
+ }
+ 
++void QHttpNetworkConnectionChannel::checkAndResumeCommunication()
++{
++    Q_ASSERT(connection->connectionType() > QHttpNetworkConnection::ConnectionTypeHTTP);
++
++    // Because HTTP/2 requires that we send a SETTINGS frame as the first thing we do, and respond
++    // to a SETTINGS frame with an ACK, we need to delay any handling until we can ensure that any
++    // effects from emitting encrypted() have been processed.
++    // This function is called after encrypted() was emitted, so check for changes.
++
++    if (!reply && spdyRequestsToSend.isEmpty())
++        abort();
++    waitingForPotentialAbort = false;
++    if (needInvokeReadyRead)
++        _q_readyRead();
++    if (needInvokeReceiveReply)
++        _q_receiveReply();
++    if (needInvokeSendRequest)
++        sendRequest();
++}
++
+ void QHttpNetworkConnectionChannel::requeueSpdyRequests()
+ {
+     QList<HttpMessagePair> spdyPairs = spdyRequestsToSend.values();
+--- a/src/network/access/qhttpnetworkconnectionchannel_p.h
++++ b/src/network/access/qhttpnetworkconnectionchannel_p.h
+@@ -107,6 +107,10 @@ public:
+     QAbstractSocket *socket;
+     bool ssl;
+     bool isInitialized;
++    bool waitingForPotentialAbort = false;
++    bool needInvokeReceiveReply = false;
++    bool needInvokeReadyRead = false;
++    bool needInvokeSendRequest = false;
+     ChannelState state;
+     QHttpNetworkRequest request; // current request, only used for HTTP
+     QHttpNetworkReply *reply; // current reply for this request, only used for HTTP
+@@ -187,6 +191,8 @@ public:
+     void closeAndResendCurrentRequest();
+     void resendCurrentRequest();
+ 
++    void checkAndResumeCommunication();
++
+     bool isSocketBusy() const;
+     bool isSocketWriting() const;
+     bool isSocketWaiting() const;
--- /dev/null
+++ b/debian/patches/a11y_null_checks.diff
@@ -0,0 +1,76 @@
+Description: a11y atspi: add null checks in table iface methods
+ Add null checks to cover the cases where QAccessibleTableInterface::cellAt
+ returns nullptr (which happens e.g. when called with invalid indices via
+ AT-SPI) or where the cell object doesn't implement the
+ QAccessibleTableCellInterface, which would previously result in crashes.
+ .
+ Cherry-picked into 5.15 as it fixes a crash in popular accessibility client
+ software. Conflict resolution: remove C++17'isms (`if` with initializer).
+Origin: upstream, https://invent.kde.org/qt/qt/qtbase/-/commit/076da096464a5d3f
+Last-Update: 2025-03-24
+Bug: https://bugs.debian.org/1081682
+
+--- a/src/platformsupport/linuxaccessibility/atspiadaptor.cpp
++++ b/src/platformsupport/linuxaccessibility/atspiadaptor.cpp
+@@ -2393,13 +2393,14 @@ bool AtSpiAdaptor::tableInterface(QAcces
+         if (cols > 0) {
+             row = index / cols;
+             col = index % cols;
+-            QAccessibleTableCellInterface *cell = interface->tableInterface()->cellAt(row, col)->tableCellInterface();
+-            if (cell) {
+-                row = cell->rowIndex();
+-                col = cell->columnIndex();
+-                rowExtents = cell->rowExtent();
+-                colExtents = cell->columnExtent();
+-                isSelected = cell->isSelected();
++            QAccessibleInterface *cell = interface->tableInterface()->cellAt(row, col);
++            QAccessibleTableCellInterface *cellIface = cell ? cell->tableCellInterface() : nullptr;
++            if (cellIface) {
++                row = cellIface->rowIndex();
++                col = cellIface->columnIndex();
++                rowExtents = cellIface->rowExtent();
++                colExtents = cellIface->columnExtent();
++                isSelected = cellIface->isSelected();
+                 success = true;
+             }
+         }
+@@ -2410,12 +2411,22 @@ bool AtSpiAdaptor::tableInterface(QAcces
+     } else if (function == QLatin1String("GetColumnExtentAt")) {
+         int row = message.arguments().at(0).toInt();
+         int column = message.arguments().at(1).toInt();
+-        connection.send(message.createReply(interface->tableInterface()->cellAt(row, column)->tableCellInterface()->columnExtent()));
++        int columnExtent = 0;
++        QAccessibleInterface *cell = interface->tableInterface()->cellAt(row, column);
++        QAccessibleTableCellInterface *cellIface = cell ? cell->tableCellInterface() : nullptr;
++        if (cellIface)
++            columnExtent = cellIface->columnExtent();
++        connection.send(message.createReply(columnExtent));
+ 
+     } else if (function == QLatin1String("GetRowExtentAt")) {
+         int row = message.arguments().at(0).toInt();
+         int column = message.arguments().at(1).toInt();
+-        connection.send(message.createReply(interface->tableInterface()->cellAt(row, column)->tableCellInterface()->rowExtent()));
++        int rowExtent = 0;
++        QAccessibleInterface *cell = interface->tableInterface()->cellAt(row, column);
++        QAccessibleTableCellInterface *cellIface = cell ? cell->tableCellInterface() : nullptr;
++        if (cellIface)
++            rowExtent = cellIface->rowExtent();
++        connection.send(message.createReply(rowExtent));
+ 
+     } else if (function == QLatin1String("GetColumnHeader")) {
+         int column = message.arguments().at(0).toInt();
+@@ -2455,8 +2466,12 @@ bool AtSpiAdaptor::tableInterface(QAcces
+     } else if (function == QLatin1String("IsSelected")) {
+         int row = message.arguments().at(0).toInt();
+         int column = message.arguments().at(1).toInt();
+-        QAccessibleTableCellInterface* cell = interface->tableInterface()->cellAt(row, column)->tableCellInterface();
+-        connection.send(message.createReply(cell->isSelected()));
++        bool isSelected = false;
++        QAccessibleInterface *cell = interface->tableInterface()->cellAt(row, column);
++        QAccessibleTableCellInterface *cellIface = cell ? cell->tableCellInterface() : nullptr;
++        if (cellIface)
++            isSelected = cellIface->isSelected();
++        connection.send(message.createReply(isSelected));
+     } else if (function == QLatin1String("AddColumnSelection")) {
+         int column = message.arguments().at(0).toInt();
+         connection.send(message.createReply(interface->tableInterface()->selectColumn(column)));
--- a/debian/patches/series
+++ b/debian/patches/series
@@ -23,9 +23,10 @@ sql_odbc_fix_unicode_check.diff
 CVE-2023-34410.diff
 CVE-2023-37369.diff
 CVE-2023-38197.diff
-
 CVE-2023-51714.diff
 CVE-2024-25580.diff
+a11y_null_checks.diff
+CVE-2024-39936.diff
 
 # Debian specific.
 gnukfreebsd.diff

Attachment: signature.asc
Description: PGP signature

Reply via email to