From c4ae6491d473e36f34840a236c5fd0f4bc800558 Mon Sep 17 00:00:00 2001 From: MÃ¥rten Nordheim Date: Mon, 03 Aug 2026 16:56:46 +0200 Subject: [PATCH] QHttp2Stream: fix use-after-free on upload device teardown mid-upload uploadDeviceDestroyed() only cleared m_uploadDevice, leaving m_uploadByteDevice dangling. If the peer later grows the stream's receive window (SETTINGS INITIAL_WINDOW_SIZE), maybeResumeUpload() resumes the upload through the now-freed QNonContiguousByteDevice, causing a heap-use-after-free with virtual dispatch on attacker- controlled timing. Also clear m_owningByteDevice so the dangling pointer is never revisited or deleted again. Changes in 6.12: - Resolved trivial merge conflict (adjecent test missing). Changes in 6.11: - Resolved conflict in test (other change not picked) Change-Id: Ia6a7810c071f46a891d460247bdde187613e152e Reviewed-by: Edward Welbourne (cherry picked from commit deedcc1b9b711ec7f32e9caf68defed4aead379f) (cherry picked from commit c60dd9c20425ce3571718b2711b05bfd9878f401) Reviewed-by: Mate Barany --- diff --git a/src/network/access/qhttp2connection.cpp b/src/network/access/qhttp2connection.cpp index 2f5d660..d75a30c 100644 --- a/src/network/access/qhttp2connection.cpp +++ b/src/network/access/qhttp2connection.cpp @@ -640,6 +640,8 @@ emit uploadDeviceError(message); } m_uploadDevice = nullptr; + m_owningByteDevice = false; + m_uploadByteDevice = nullptr; } void QHttp2Stream::setState(State newState) diff --git a/tests/auto/network/access/qhttp2connection/CMakeLists.txt b/tests/auto/network/access/qhttp2connection/CMakeLists.txt index 9a6e7a0..4be5807 100644 --- a/tests/auto/network/access/qhttp2connection/CMakeLists.txt +++ b/tests/auto/network/access/qhttp2connection/CMakeLists.txt @@ -11,6 +11,7 @@ SOURCES tst_qhttp2connection.cpp LIBRARIES + Qt::CorePrivate Qt::NetworkPrivate Qt::Test ) diff --git a/tests/auto/network/access/qhttp2connection/tst_qhttp2connection.cpp b/tests/auto/network/access/qhttp2connection/tst_qhttp2connection.cpp index 5af36fb..0226a37 100644 --- a/tests/auto/network/access/qhttp2connection/tst_qhttp2connection.cpp +++ b/tests/auto/network/access/qhttp2connection/tst_qhttp2connection.cpp @@ -9,8 +9,10 @@ #include #include +#include #include #include +#include // for uploadByteDeviceDestroyedWhileBlocked #include @@ -49,6 +51,7 @@ void serverInitiatedGoaways_data(); void serverInitiatedGoaways(); void clientInitiatedGoaway(); + void uploadByteDeviceDestroyedWhileBlocked(); private: enum PeerType { Client, Server }; @@ -1616,6 +1619,69 @@ QTRY_COMPARE(serverClosedSpy.count(), 1); } +void tst_QHttp2Connection::uploadByteDeviceDestroyedWhileBlocked() +{ + // uploadDeviceDestroyed() used to clear m_uploadDevice only, leaving + // m_uploadByteDevice dangling. A later SETTINGS INITIAL_WINDOW_SIZE + // increase from the peer would then queue maybeResumeUpload(), which + // dereferences the freed device (heap-use-after-free). + auto [client, server] = makeFakeConnectedSockets(); + auto clientConn = makeHttp2Connection(client.get(), {}, Client); + + // Creating the stream flushes the client preface; handleReadyRead() + // ignores incoming frames until that has happened. + QHttp2Stream *clientStream = clientConn->createStream().unwrap(); + QVERIFY(clientStream); + + // Shrink the per-stream send window so a 4096-byte upload stalls almost + // immediately, well before the device is exhausted. There's no public + // API for this, so inject the SETTINGS frame directly. + { + Http2::FrameWriter s(Http2::FrameType::SETTINGS, Http2::FrameFlag::EMPTY, + Http2::connectionStreamID); + s.append(Http2::Settings::INITIAL_WINDOW_SIZE_ID); + s.append(quint32(64)); + QVERIFY(s.write(*server)); + } + QVERIFY(QTest::qWaitFor([&]() { return clientStream->m_sendWindow == 64; })); + + QVERIFY(clientStream->sendHEADERS(getRequiredHeaders(), false)); + + auto *buffer = new QBuffer; + buffer->setData(QByteArray(4096, 'A')); + buffer->open(QIODevice::ReadOnly); + auto *uploadDevice = QNonContiguousByteDeviceFactory::create(buffer); + buffer->setParent(uploadDevice); + + QSignalSpy uploadBlockedSpy{ clientStream, &QHttp2Stream::uploadBlocked }; + // This is the overload of sendDATA() QNetworkAccessManager's HTTP/2 handler uses: the + // caller (not QHttp2Stream) owns the device's lifetime. Because we can't send everything in one + // go it will return early and try to send more later. + QVERIFY(clientStream->sendDATA(uploadDevice, true)); + QCOMPARE(uploadBlockedSpy.count(), 1); + QVERIFY(clientStream->isUploadingDATA()); + + // The upload source is destroyed while the upload is window-stalled and + // the stream object survives. + delete uploadDevice; + QVERIFY(!clientStream->isUploadingDATA()); + + // Grow the window again, as if resuming a live upload. + { + Http2::FrameWriter s(Http2::FrameType::SETTINGS, Http2::FrameFlag::EMPTY, + Http2::connectionStreamID); + s.append(Http2::Settings::INITIAL_WINDOW_SIZE_ID); + s.append(quint32(1024 * 1024)); + QVERIFY(s.write(*server)); + } + // Processing the frame above queues a maybeResumeUpload() call that runs + // during this wait. This would crash, reading through the already-freed + // device's vtable, before this patch. + QVERIFY(QTest::qWaitFor( + [&]() { return clientConn->streamInitialSendWindowSize == 1024 * 1024; })); + QVERIFY(!clientConn->m_connectionAborted); +} + QTEST_MAIN(tst_QHttp2Connection) #include "tst_qhttp2connection.moc"