Submitted By: Douglas R. Reno Date: 2025-07-27 Initial Package Version: 6.9.1 Origin: Upstream (commits eb6df1d, f6eb24d5, 1fe3a3c05, and the official CVE-2025-5992 patch listed in #21858) Upstream Status: Applied Description: Combines several upstream patches from the 6.9 branch of qtbase upstream to fix crashes with KDE Plasma 6.4 and fix a low-severity security vulnerability known as CVE-2025-5992. See Ticket #21858 for more details. diff -Naurp qt-everywhere-src-6.9.1.orig/qtbase/src/gui/painting/qcolortransfergeneric_p.h qt-everywhere-src-6.9.1/qtbase/src/gui/painting/qcolortransfergeneric_p.h --- qt-everywhere-src-6.9.1.orig/qtbase/src/gui/painting/qcolortransfergeneric_p.h 2025-07-27 22:09:18.746554057 -0500 +++ qt-everywhere-src-6.9.1/qtbase/src/gui/painting/qcolortransfergeneric_p.h 2025-07-27 22:10:31.524148365 -0500 @@ -65,6 +65,7 @@ private: // HLG from linear [0-12] -> [0-1] static float hlgFromLinear(float x) { + x = std::clamp(x, 0.f, 12.f); if (x > 1.f) return m_hlg_a * std::log(x - m_hlg_b) + m_hlg_c; return std::sqrt(x * 0.25f); @@ -73,6 +74,7 @@ private: // HLG to linear [0-1] -> [0-12] static float hlgToLinear(float x) { + x = std::clamp(x, 0.f, 1.f); if (x < 0.5f) return (x * x) * 4.f; return std::exp((x - m_hlg_c) / m_hlg_a) + m_hlg_b; @@ -86,6 +88,7 @@ private: // PQ to linear [0-1] -> [0-64] static float pqToLinear(float e) { + e = std::clamp(e, 0.f, 1.f); // m2-th root of E' const float eRoot = std::pow(e, 1.f / m_pq_m2); // rational transform @@ -99,6 +102,7 @@ private: // PQ from linear [0-64] -> [0-1] static float pqFromLinear(float fd) { + fd = std::clamp(fd, 0.f, 64.f); // scale Fd to Y const float y = fd * (1.f / m_pq_f); // yRoot = Y^m1 -- "root" because m1 is <1 diff -Naurp qt-everywhere-src-6.9.1.orig/qtbase/src/network/access/qhttp2connection.cpp qt-everywhere-src-6.9.1/qtbase/src/network/access/qhttp2connection.cpp --- qt-everywhere-src-6.9.1.orig/qtbase/src/network/access/qhttp2connection.cpp 2025-07-27 22:09:18.761553974 -0500 +++ qt-everywhere-src-6.9.1/qtbase/src/network/access/qhttp2connection.cpp 2025-07-27 22:13:51.391018848 -0500 @@ -1384,13 +1384,16 @@ void QHttp2Connection::handleDATA() if (isInvalidStream(streamID)) return connectionError(ENHANCE_YOUR_CALM, "DATA on invalid stream"); - // RFC9113, 6.1: If a DATA frame is received whose stream is not in the "open" or - // "half-closed (local)" state, the recipient MUST respond with a stream error. - auto stream = getStream(streamID); - if (stream->state() == QHttp2Stream::State::HalfClosedRemote - || stream->state() == QHttp2Stream::State::Closed) { - return stream->streamError(Http2Error::STREAM_CLOSED, - QLatin1String("Data on closed stream")); + QHttp2Stream *stream = nullptr; + if (!streamWasResetLocally(streamID)) { + stream = getStream(streamID); + // RFC9113, 6.1: If a DATA frame is received whose stream is not in the "open" or + // "half-closed (local)" state, the recipient MUST respond with a stream error. + if (stream->state() == QHttp2Stream::State::HalfClosedRemote + || stream->state() == QHttp2Stream::State::Closed) { + return stream->streamError(Http2Error::STREAM_CLOSED, + QLatin1String("Data on closed stream")); + } } if (qint32(inboundFrame.payloadSize()) > sessionReceiveWindowSize) { @@ -1403,9 +1406,8 @@ void QHttp2Connection::handleDATA() sessionReceiveWindowSize -= inboundFrame.payloadSize(); - auto it = m_streams.constFind(streamID); - if (it != m_streams.cend() && it.value()) - it.value()->handleDATA(inboundFrame); + if (stream) + stream->handleDATA(inboundFrame); if (inboundFrame.flags().testFlag(FrameFlag::END_STREAM)) emit receivedEND_STREAM(streamID); @@ -1451,6 +1453,11 @@ void QHttp2Connection::handleHEADERS() qCDebug(qHttp2ConnectionLog, "[%p] Created new incoming stream %d", this, streamID); emit newIncomingStream(newStream); + } else if (streamWasResetLocally(streamID)) { + qCDebug(qHttp2ConnectionLog, + "[%p] Received HEADERS on previously locally reset stream %d (must process but ignore)", + this, streamID); + // nop } else if (auto it = m_streams.constFind(streamID); it == m_streams.cend()) { // RFC 9113, 6.2: HEADERS frames MUST be associated with a stream. // A connection error is not required but it seems to be the right thing to do. @@ -1923,8 +1930,8 @@ void QHttp2Connection::handleContinuedHE return connectionError(FRAME_SIZE_ERROR, "HEADERS frame too large"); } - if (streamIt == m_streams.cend()) // No more processing without a stream from here on. - return; + if (streamWasResetLocally(streamID) || streamIt == m_streams.cend()) + return; // No more processing without a stream from here on. switch (firstFrameType) { case FrameType::HEADERS: diff -Naurp qt-everywhere-src-6.9.1.orig/qtbase/src/network/access/qhttp2protocolhandler.cpp qt-everywhere-src-6.9.1/qtbase/src/network/access/qhttp2protocolhandler.cpp --- qt-everywhere-src-6.9.1.orig/qtbase/src/network/access/qhttp2protocolhandler.cpp 2025-07-27 22:09:18.761553974 -0500 +++ qt-everywhere-src-6.9.1/qtbase/src/network/access/qhttp2protocolhandler.cpp 2025-07-27 22:13:51.391018848 -0500 @@ -265,6 +265,7 @@ bool QHttp2ProtocolHandler::tryRemoveRep { QHttp2Stream *stream = streamIDs.take(reply); if (stream) { + stream->sendRST_STREAM(stream->isUploadingDATA() ? Http2::CANCEL : Http2::HTTP2_NO_ERROR); requestReplyPairs.remove(stream); stream->deleteLater(); return true; @@ -307,10 +308,12 @@ bool QHttp2ProtocolHandler::sendDATA(QHt void QHttp2ProtocolHandler::handleHeadersReceived(const HPack::HttpHeader &headers, bool endStream) { QHttp2Stream *stream = qobject_cast(sender()); + Q_ASSERT(stream); auto &requestPair = requestReplyPairs[stream]; auto *httpReply = requestPair.second; auto &httpRequest = requestPair.first; - Q_ASSERT(httpReply || stream->state() == QHttp2Stream::State::ReservedRemote); + if (!httpReply) + return; auto *httpReplyPrivate = httpReply->d_func(); @@ -393,6 +396,8 @@ void QHttp2ProtocolHandler::handleDataRe QHttp2Stream *stream = qobject_cast(sender()); auto &httpPair = requestReplyPairs[stream]; auto *httpReply = httpPair.second; + if (!httpReply) + return; Q_ASSERT(!stream->isPromisedStream()); if (!data.isEmpty() && !httpPair.first.d->needResendWithCredentials) { diff -Naurp qt-everywhere-src-6.9.1.orig/qtbase/tests/auto/network/access/http2/tst_http2.cpp qt-everywhere-src-6.9.1/qtbase/tests/auto/network/access/http2/tst_http2.cpp --- qt-everywhere-src-6.9.1.orig/qtbase/tests/auto/network/access/http2/tst_http2.cpp 2025-07-27 22:09:19.086552170 -0500 +++ qt-everywhere-src-6.9.1/qtbase/tests/auto/network/access/http2/tst_http2.cpp 2025-07-27 22:12:53.688346945 -0500 @@ -88,6 +88,7 @@ private slots: void goaway(); void earlyResponse(); void earlyError(); + void abortReply(); void connectToHost_data(); void connectToHost(); void maxFrameSize(); @@ -774,6 +775,69 @@ void tst_Http2::earlyError() QTRY_VERIFY(serverGotSettingsACK); } +/* + As above this test relies a bit on timing so we are + using QHttpNetworkRequest directly. +*/ +void tst_Http2::abortReply() +{ + clearHTTP2State(); + serverPort = 0; + + const auto serverConnectionType = defaultConnectionType() == H2Type::h2c ? H2Type::h2Direct + : H2Type::h2Alpn; + ServerPtr targetServer(newServer(defaultServerSettings, serverConnectionType)); + + QMetaObject::invokeMethod(targetServer.data(), "startServer", Qt::QueuedConnection); + runEventLoop(); + + QVERIFY(serverPort != 0); + + nRequests = 1; + + // SETUP create QHttpNetworkConnection primed for http2 usage + const auto connectionType = serverConnectionType == H2Type::h2Direct + ? QHttpNetworkConnection::ConnectionTypeHTTP2Direct + : QHttpNetworkConnection::ConnectionTypeHTTP2; + QHttpNetworkConnection connection(1, "127.0.0.1", serverPort, true, false, nullptr, + connectionType); + QSslConfiguration config = QSslConfiguration::defaultConfiguration(); + config.setAllowedNextProtocols({"h2"}); + connection.setSslConfiguration(config); + connection.ignoreSslErrors(); + + // SETUP manually setup the QHttpNetworkRequest + QHttpNetworkRequest req; + req.setSsl(true); + req.setHTTP2Allowed(true); + if (defaultConnectionType() == H2Type::h2c) + req.setH2cAllowed(true); + req.setOperation(QHttpNetworkRequest::Post); + req.setUrl(requestUrl(defaultConnectionType())); + // ^ All the above is set-up, the real code starts below v + + std::unique_ptr reply{connection.sendRequest(req)}; + QVERIFY(reply); + QSemaphore sem; + QObject::connect(reply.get(), &QHttpNetworkReply::requestSent, reply.get(), [&](){ + reply.reset(); + sem.release(); + }); + + // failOnWarning doesn't work for qCritical, so we set this env-var: + const char envvar[] = "QT_FATAL_CRITICALS"; + auto restore = qScopeGuard([envvar, prev = qgetenv(envvar)]() { + qputenv(envvar, prev); + }); + qputenv(envvar, "1"); + QTest::failOnWarning(QRegularExpression("HEADERS on invalid stream")); + QVERIFY(QTest::qWaitFor([&sem]() { return sem.tryAcquire(); })); + using namespace std::chrono_literals; + // Process some extra events in case they trigger an error: + QTest::qWait(100ms); +} + + void tst_Http2::connectToHost_data() { // The attribute to set on a new request: diff -Naurp qt-everywhere-src-6.9.1.orig/qtbase/tests/auto/network/access/qhttp2connection/tst_qhttp2connection.cpp qt-everywhere-src-6.9.1/qtbase/tests/auto/network/access/qhttp2connection/tst_qhttp2connection.cpp --- qt-everywhere-src-6.9.1.orig/qtbase/tests/auto/network/access/qhttp2connection/tst_qhttp2connection.cpp 2025-07-27 22:09:19.087552164 -0500 +++ qt-everywhere-src-6.9.1/qtbase/tests/auto/network/access/qhttp2connection/tst_qhttp2connection.cpp 2025-07-27 22:13:51.392018843 -0500 @@ -30,6 +30,7 @@ private slots: void testBadFrameSize(); void testDataFrameAfterRSTIncoming(); void testDataFrameAfterRSTOutgoing(); + void headerFrameAfterRSTOutgoing_data(); void headerFrameAfterRSTOutgoing(); void connectToServer(); void WINDOW_UPDATE(); @@ -728,14 +729,23 @@ void tst_QHttp2Connection::testDataFrame QVERIFY(closedServerSpy.wait()); } +void tst_QHttp2Connection::headerFrameAfterRSTOutgoing_data() +{ + QTest::addColumn("deleteStream"); + QTest::addRow("retain-stream") << false; + QTest::addRow("delete-stream") << true; +} + void tst_QHttp2Connection::headerFrameAfterRSTOutgoing() { + QFETCH(const bool, deleteStream); auto [client, server] = makeFakeConnectedSockets(); auto *connection = makeHttp2Connection(client.get(), {}, Client); auto *serverConnection = makeHttp2Connection(server.get(), {}, Server); QHttp2Stream *clientStream = connection->createStream().unwrap(); QVERIFY(clientStream); + QSignalSpy client1HeadersSpy{ clientStream, &QHttp2Stream::headersReceived}; QVERIFY(waitForSettingsExchange(connection, serverConnection)); QSignalSpy newIncomingStreamSpy{ serverConnection, &QHttp2Connection::newIncomingStream }; @@ -753,6 +763,8 @@ void tst_QHttp2Connection::headerFrameAf // Send an RST frame from the client, but we don't process it yet clientStream->sendRST_STREAM(Http2::CANCEL); + if (deleteStream) + delete std::exchange(clientStream, nullptr); // The server sends a reply, not knowing about the inbound RST frame const HPack::HttpHeader StandardReply{ { ":status", "200" }, { "x-whatever", "some info" } }; @@ -763,6 +775,9 @@ void tst_QHttp2Connection::headerFrameAf // causing an error on Qt's side. QVERIFY(serverRSTReceivedSpy.wait()); + // We don't emit any headers for a reset stream + QVERIFY(!client1HeadersSpy.count()); + // Create a new stream then send and handle a new request! QHttp2Stream *clientStream2 = connection->createStream().unwrap(); QSignalSpy client2HeaderReceivedSpy{ clientStream2, &QHttp2Stream::headersReceived };