diff --git a/net/spdy/spdy_read_queue_unittest.cc b/net/spdy/spdy_read_queue_unittest.cc index 03e89802c7a0f..7a040eef28288 100644 --- a/net/spdy/spdy_read_queue_unittest.cc +++ b/net/spdy/spdy_read_queue_unittest.cc @@ -5,10 +5,12 @@ #include "net/spdy/spdy_read_queue.h" #include +#include #include #include #include #include +#include #include "base/containers/heap_array.h" #include "base/containers/span.h" @@ -138,4 +140,124 @@ TEST_F(SpdyReadQueueTest, Clear) { EXPECT_TRUE(read_queue.IsEmpty()); } +// ============================================================================ +// POC: heap-use-after-free in SpdyBuffer::ConsumeHelper via reentrant Dequeue +// ============================================================================ +// +// This test models the reentrancy that occurs in production when a malicious +// HTTP/2 proxy triggers SpdyProxyClientSocket::OnClose() from inside the +// SpdyBuffer consume-callback chain (via the queue-overflow → DoDrainSession → +// StartGoingAway → stream OnClose → OnDataReceived(nullptr) path). +// +// Outer Dequeue:48 calls buffer->Consume() because the user buffer is smaller +// than the SpdyBuffer (the "Consume() variant" path). ConsumeHelper iterates +// consume_callbacks_. The first callback re-enters Dequeue with a buffer large +// enough to drain the rest, hitting Dequeue:46 pop_front → ~SpdyBuffer → +// operator delete. Control unwinds to outer ConsumeHelper:108, which then +// reads consume_callbacks_.end() on a freed `this`. +// +// Two callbacks are registered to mirror the production case where +// SpdySession::OnReadBufferConsumed (the destroyer) is registered before +// SpdyStream::OnReadBufferConsumed (read from freed vector storage on the +// next iteration). +namespace { + +// Simulates SpdySession::OnReadBufferConsumed → IncreaseRecvWindowSize → +// SendWindowUpdateFrame → EnqueueSessionWrite (queue full) → DoDrainSession → +// StartGoingAway → CloseActiveStreamIterator → SpdyStream::OnClose → +// SpdyProxyClientSocket::OnClose:637 → OnDataReceived(nullptr) → +// PopulateUserReadBuffer → SpdyReadQueue::Dequeue (reentrant). +// +// Only fires once (on the outer CONSUME). The inner ~SpdyBuffer's +// ConsumeHelper(DISCARD) re-invokes us, but by that point the production +// chain is a no-op (session DRAINING, stream WeakPtr dead) so we no-op too. +void ReentrantDequeue(SpdyReadQueue* queue, + bool* fired, + size_t inner_buf_len, + size_t consume_size, + SpdyBuffer::ConsumeSource consume_source) { + if (*fired) { + // Inner ~SpdyBuffer:78 ConsumeHelper(DISCARD) lands here. In production + // this is the second pass through OnReadBufferConsumed where DoDrainSession + // early-returns at :2675 (STATE_DRAINING). No-op. + return; + } + *fired = true; + + // Inner OnDataReceived → PopulateUserReadBuffer → Dequeue. + // user_buffer_len_ is unchanged, so the inner span is the same size as the + // outer. Since outer Consume already advanced offset_ by user_buffer_len_, + // GetRemainingSize() = P - user_buffer_len_ <= user_buffer_len_, so + // inner Dequeue:45 (bytes_to_copy == GetRemainingSize()) → pop_front. + std::vector inner_buf(inner_buf_len); + queue->Dequeue(base::span(inner_buf)); + // ~SpdyBuffer has now run. The SpdyBuffer object that the outer + // ConsumeHelper is iterating over is freed. +} + +// Simulates SpdyStream::OnReadBufferConsumed (registered second at +// spdy_stream.cc:472). After the buffer is freed, outer ConsumeHelper:108 +// `++it; it != consume_callbacks_.end()` reads freed `this`. If the +// comparison passes, `it->Run()` reads this callback's freed BindState and +// calls a freed function pointer. +void SecondCallback(int* counter, + size_t consume_size, + SpdyBuffer::ConsumeSource consume_source) { + // In production this is SpdyStream::OnReadBufferConsumed via a WeakPtr + // that is already invalidated by the time of the second (DISCARD) call. + // We touch heap-bound state to make the UAF observable even if the + // freed-vector-storage read happens to hold a stale-but-callable thunk. + (*counter)++; +} + +} // namespace + +TEST_F(SpdyReadQueueTest, ConsumeCallbackReentrantDequeueUAF) { + // P = 20, user_buffer_len_ = 12. Then user_buffer_len_ < P <= 2*user_buffer_len_ + // → outer Dequeue takes the Consume() branch at :48 + // → inner Dequeue sees GetRemainingSize() = 8 <= 12, takes pop_front at :46. + constexpr size_t kPayloadSize = 20; + constexpr size_t kUserBufLen = 12; + static_assert(kUserBufLen < kPayloadSize, "must hit Consume branch"); + static_assert(kPayloadSize <= 2 * kUserBufLen, "inner must hit pop_front"); + + std::array payload; + payload.fill(0x41); + + SpdyReadQueue queue; + auto buffer = std::make_unique(base::span(payload)); + + bool reentry_fired = false; + int second_cb_counter = 0; + + // Callback registration order matches production: SpdySession's callback + // (the destroyer) is added at spdy_session.cc:2966 BEFORE SpdyStream's + // callback at spdy_stream.cc:472. + buffer->AddConsumeCallback(base::BindRepeating( + &ReentrantDequeue, &queue, &reentry_fired, kUserBufLen)); + buffer->AddConsumeCallback( + base::BindRepeating(&SecondCallback, &second_cb_counter)); + + queue.Enqueue(std::move(buffer)); + + // Outer SpdyProxyClientSocket::OnDataReceived → PopulateUserReadBuffer → + // Dequeue with user_buffer_len_ = 12. + std::array user_buf; + size_t copied = queue.Dequeue(base::span(user_buf)); + + // ===== EXECUTION DOES NOT REACH HERE UNDER ASAN ===== + // ASAN detects heap-use-after-free at spdy_buffer.cc:108 when outer + // ConsumeHelper evaluates `it != consume_callbacks_.end()` on freed `this`. + // + // Without ASAN: undefined behavior. The freed SpdyBuffer's vtable-less + // members are read; depending on allocator state, the loop may terminate + // (stale end() == ++it), or it may dereference a freed RepeatingCallback's + // BindState and call its polymorphic_invoke_ function pointer. + (void)copied; + ADD_FAILURE() << "Expected ASAN heap-use-after-free in " + "SpdyBuffer::ConsumeHelper, but execution continued. " + "reentry_fired=" << reentry_fired + << " second_cb_counter=" << second_cb_counter; +} + } // namespace net::test