Submitted By:            Douglas R. Reno <renodr at linuxfromscratch dot org>
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<QHttp2Stream *>(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<QHttp2Stream *>(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<QHttpNetworkReply> 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<bool>("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 };
