123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248 |
- From 6d2a5ad09074bd77b2de09adf7147107ee0db026 Mon Sep 17 00:00:00 2001
- From: =?UTF-8?q?M=C3=A5rten=20Nordheim?= <marten.nordheim@qt.io>
- Date: Tue, 25 Jun 2024 17:09:35 +0200
- Subject: [PATCH] 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.
- Fixes: QTBUG-126610
- Pick-to: 6.5 6.2 5.15 5.12
- Change-Id: Ic25a600c278203256e35f541026f34a8783235ae
- Reviewed-by: Marc Mutz <marc.mutz@qt.io>
- Reviewed-by: Volker Hilsheimer <volker.hilsheimer@qt.io>
- (cherry picked from commit b1e75376cc3adfc7da5502a277dfe9711f3e0536)
- Reviewed-by: Qt Cherry-pick Bot <cherrypick_bot@qt-project.org>
- (cherry picked from commit 0fb43e4395da34d561814242a0186999e4956e28)
- Fixes: https://security-tracker.debian.org/tracker/CVE-2024-39936
- Upstream: https://github.com/qt/qtbase/commit/2b1e36e183ce75c224305c7a94457b92f7a5cf58
- Signed-off-by: Thomas Petazzoni <thomas.petazzoni@bootlin.com>
- ---
- src/network/access/qhttp2protocolhandler.cpp | 6 +--
- .../access/qhttpnetworkconnectionchannel.cpp | 48 ++++++++++++++++++-
- .../access/qhttpnetworkconnectionchannel_p.h | 6 +++
- tests/auto/network/access/http2/tst_http2.cpp | 44 +++++++++++++++++
- 4 files changed, 99 insertions(+), 5 deletions(-)
- diff --git a/src/network/access/qhttp2protocolhandler.cpp b/src/network/access/qhttp2protocolhandler.cpp
- index 562b883242f..8c543efc742 100644
- --- a/src/network/access/qhttp2protocolhandler.cpp
- +++ b/src/network/access/qhttp2protocolhandler.cpp
- @@ -335,12 +335,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?
- diff --git a/src/network/access/qhttpnetworkconnectionchannel.cpp b/src/network/access/qhttpnetworkconnectionchannel.cpp
- index 1e036bdc450..a64d1a25898 100644
- --- a/src/network/access/qhttpnetworkconnectionchannel.cpp
- +++ b/src/network/access/qhttpnetworkconnectionchannel.cpp
- @@ -209,6 +209,10 @@ void QHttpNetworkConnectionChannel::abort()
- bool QHttpNetworkConnectionChannel::sendRequest()
- {
- Q_ASSERT(protocolHandler);
- + if (waitingForPotentialAbort) {
- + needInvokeSendRequest = true;
- + return false; // this return value is unused
- + }
- return protocolHandler->sendRequest();
- }
-
- @@ -221,21 +225,28 @@ bool QHttpNetworkConnectionChannel::sendRequest()
- void QHttpNetworkConnectionChannel::sendRequestDelayed()
- {
- QMetaObject::invokeMethod(this, [this] {
- - Q_ASSERT(protocolHandler);
- if (reply)
- - protocolHandler->sendRequest();
- + sendRequest();
- }, Qt::ConnectionType::QueuedConnection);
- }
-
- void QHttpNetworkConnectionChannel::_q_receiveReply()
- {
- Q_ASSERT(protocolHandler);
- + if (waitingForPotentialAbort) {
- + needInvokeReceiveReply = true;
- + return;
- + }
- protocolHandler->_q_receiveReply();
- }
-
- void QHttpNetworkConnectionChannel::_q_readyRead()
- {
- Q_ASSERT(protocolHandler);
- + if (waitingForPotentialAbort) {
- + needInvokeReadyRead = true;
- + return;
- + }
- protocolHandler->_q_readyRead();
- }
-
- @@ -1232,7 +1243,18 @@ void QHttpNetworkConnectionChannel::_q_encrypted()
- // Similar to HTTP/1.1 counterpart below:
- const auto &h2Pairs = h2RequestsToSend.values(); // (request, reply)
- const auto &pair = h2Pairs.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);
- @@ -1250,6 +1272,28 @@ void QHttpNetworkConnectionChannel::_q_encrypted()
- }
- }
-
- +
- +void QHttpNetworkConnectionChannel::checkAndResumeCommunication()
- +{
- + Q_ASSERT(connection->connectionType() == QHttpNetworkConnection::ConnectionTypeHTTP2
- + || connection->connectionType() == QHttpNetworkConnection::ConnectionTypeHTTP2Direct);
- +
- + // 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 && h2RequestsToSend.isEmpty())
- + abort();
- + waitingForPotentialAbort = false;
- + if (needInvokeReadyRead)
- + _q_readyRead();
- + if (needInvokeReceiveReply)
- + _q_receiveReply();
- + if (needInvokeSendRequest)
- + sendRequest();
- +}
- +
- void QHttpNetworkConnectionChannel::requeueHttp2Requests()
- {
- QList<HttpMessagePair> h2Pairs = h2RequestsToSend.values();
- diff --git a/src/network/access/qhttpnetworkconnectionchannel_p.h b/src/network/access/qhttpnetworkconnectionchannel_p.h
- index 0772a4452b3..2a5336ca8a4 100644
- --- a/src/network/access/qhttpnetworkconnectionchannel_p.h
- +++ b/src/network/access/qhttpnetworkconnectionchannel_p.h
- @@ -73,6 +73,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
- @@ -145,6 +149,8 @@ public:
- void closeAndResendCurrentRequest();
- void resendCurrentRequest();
-
- + void checkAndResumeCommunication();
- +
- bool isSocketBusy() const;
- bool isSocketWriting() const;
- bool isSocketWaiting() const;
- diff --git a/tests/auto/network/access/http2/tst_http2.cpp b/tests/auto/network/access/http2/tst_http2.cpp
- index 9c1a63bae1e..30abbb0d3ed 100644
- --- a/tests/auto/network/access/http2/tst_http2.cpp
- +++ b/tests/auto/network/access/http2/tst_http2.cpp
- @@ -99,6 +99,8 @@ private slots:
- void redirect_data();
- void redirect();
-
- + void abortOnEncrypted();
- +
- protected slots:
- // Slots to listen to our in-process server:
- void serverStarted(quint16 port);
- @@ -1280,6 +1282,48 @@ void tst_Http2::redirect()
- QTRY_VERIFY(serverGotSettingsACK);
- }
-
- +void tst_Http2::abortOnEncrypted()
- +{
- +#if !QT_CONFIG(ssl)
- + QSKIP("TLS support is needed for this test");
- +#else
- + clearHTTP2State();
- + serverPort = 0;
- +
- + ServerPtr targetServer(newServer(defaultServerSettings, H2Type::h2Direct));
- +
- + QMetaObject::invokeMethod(targetServer.data(), "startServer", Qt::QueuedConnection);
- + runEventLoop();
- +
- + nRequests = 1;
- + nSentRequests = 0;
- +
- + const auto url = requestUrl(H2Type::h2Direct);
- + QNetworkRequest request(url);
- + request.setAttribute(QNetworkRequest::Http2DirectAttribute, true);
- +
- + std::unique_ptr<QNetworkReply> reply{manager->get(request)};
- + reply->ignoreSslErrors();
- + connect(reply.get(), &QNetworkReply::encrypted, reply.get(), [reply = reply.get()](){
- + reply->abort();
- + });
- + connect(reply.get(), &QNetworkReply::errorOccurred, this, &tst_Http2::replyFinishedWithError);
- +
- + runEventLoop();
- + STOP_ON_FAILURE
- +
- + QCOMPARE(nRequests, 0);
- + QCOMPARE(reply->error(), QNetworkReply::OperationCanceledError);
- +
- + const bool res = QTest::qWaitFor(
- + [this, server = targetServer.get()]() {
- + return serverGotSettingsACK || prefaceOK || nSentRequests > 0;
- + },
- + 500);
- + QVERIFY(!res);
- +#endif // QT_CONFIG(ssl)
- +}
- +
- void tst_Http2::serverStarted(quint16 port)
- {
- serverPort = port;
- --
- 2.46.0
|