CVE-2026-11030
Overview
Files Changed
net/spdy/spdy_session.cc
Patch
From f97b03e5e4190f540864d1e4ebc1e7ca18247862 Mon Sep 17 00:00:00 2001 From: Takashi Sakamoto <[email protected]> Date: Mon, 06 Apr 2026 21:44:53 -0700 Subject: [PATCH] Re-look-up the stream id after EnqueueResetStreamFrame() at ResetStreamIterator() EnqueueResetStreamFrame() can synchronously call DoDrainSession() -> StartGoingAway() -> CloseActiveStreamIterator(), which erases entries from `active_streams_` and invalidates `it`. So we will re-look-up the stream by ID; if it was already closed by the drain, there is nothing left to do. Bug: 497722502 Change-Id: Iee88f8e6a94e2b91832e92748f779119607f0825 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7718765 Reviewed-by: Kenichi Ishibashi <[email protected]> Commit-Queue: Takashi Sakamoto <[email protected]> Cr-Commit-Position: refs/heads/main@{#1610545} --- diff --git a/net/spdy/spdy_session.cc b/net/spdy/spdy_session.cc index b81a1c76..c5a0d3f2 100644 --- a/net/spdy/spdy_session.cc +++ b/net/spdy/spdy_session.cc @@ -1894,8 +1894,15 @@ RequestPriority priority = it->second->priority(); EnqueueResetStreamFrame(stream_id, priority, error_code, description); - // Removes any pending writes for the stream except for possibly an - // in-flight one. + // EnqueueResetStreamFrame() can synchronously call DoDrainSession() -> + // StartGoingAway() -> CloseActiveStreamIterator(), which erases entries + // from `active_streams_` and invalidates `it`. Re-look-up the stream by + // ID; if it was already closed by the drain, there is nothing left to do. + it = active_streams_.find(stream_id); + if (it == active_streams_.end()) { + return; + } + CloseActiveStreamIterator(it, error); }
Original Bug Report
Use-After-Free in SpdySession::ResetStreamIterator via synchronous DoDrainSession
Project Fortify, an experimental security project, has identified the following potential security issue.
Overview: A potential Use-After-Free vulnerability exists in SpdySession::ResetStreamIterator due to iterator invalidation when a session drain is triggered synchronously. An attacker can exploit this by filling the write queue to its limit, causing a subsequent stream reset to trigger a session-wide cleanup that erases the stream being processed.
Affected files:
net/spdy/spdy_session.ccnet/spdy/spdy_session.h
Estimated timestamp from git blame: 2023-07-05
Summary
A potential Use-After-Free (UAF) vulnerability exists in net/spdy/spdy_session.cc within the SpdySession::ResetStreamIterator function. The vulnerability occurs because the function holds a std::map::iterator (ActiveStreamMap::iterator) across a call to EnqueueResetStreamFrame. If the number of queued capped frames exceeds the predefined limit (session_max_queued_capped_frames_, default 10,000), this call synchronously triggers a session drain. The drain process erases all active streams from the session’s active_streams_ map, destroying the node the iterator points to. When control returns to ResetStreamIterator, it uses the now-invalidated iterator to call CloseActiveStreamIterator, leading to a UAF.
Technical Details
In SpdySession::ResetStreamIterator (net/spdy/spdy_session.cc), an iterator it to the active_streams_ map is captured by value. The function calls EnqueueResetStreamFrame:
void SpdySession::ResetStreamIterator(ActiveStreamMap::iterator it,
int error,
const std::string& description) {
// ...
spdy::SpdyStreamId stream_id = it->first;
RequestPriority priority = it->second->priority();
EnqueueResetStreamFrame(stream_id, priority, error_code, description); // [1]
// Removes any pending writes for the stream except for possibly an
// in-flight one.
CloseActiveStreamIterator(it, error); // [2] UAF occurs here
}
[1] EnqueueResetStreamFrame invokes EnqueueSessionWrite, which checks if the write queue has exceeded its capacity:
if (write_queue_.num_queued_capped_frames() >
session_max_queued_capped_frames_) {
LOG(WARNING)
<< "Draining session due to exceeding max queued capped frames";
DoDrainSession(ERR_CONNECTION_CLOSED, "Exceeded max queued capped frames");
return;
}
If this condition is met, it synchronously calls DoDrainSession.
[2] DoDrainSession calls StartGoingAway(0, err), which iterates through all entries in active_streams_ and synchronously calls CloseActiveStreamIterator on each, effectively erasing them from the map. This destroys the std::map node that the iterator it points to.
When EnqueueResetStreamFrame returns, ResetStreamIterator resumes and calls CloseActiveStreamIterator(it, error) with the dangling iterator. This leads to a UAF read of it->second to retrieve the SpdyStream*, a UAF write during active_streams_.erase(it) (libc++ tree rebalancing), and an arbitrary free/control flow hijack via the virtual call stream->OnClose(status) within DeleteStream.
MiraclePtr (BackupRefPtr) does not protect the std::map internal tree nodes nor the raw SpdyStream* pointers stored within the map, rendering this fully exploitable.
Potential Attack Scenario
Our tooling agent doesn’t yet have the ability to run code, but the following are the potential steps an attacker-controlled HTTP/2 server would follow to exploit this:
- Keep at least one HTTP/2 stream active.
- Stall TCP reads (e.g., advertise a TCP zero-window) to stall Chrome’s outgoing
socket_->Write()operations, causing outgoing frames to queue. - Send exactly 10,001
PINGframes (withACK=0). Because of the strict>comparison inEnqueueSessionWrite, the 10,001st frame is queued without triggering the drain. - Send a
WINDOW_UPDATEframe with an invaliddelta_window_sizeof0for the active stream. Chrome callsResetStreamIteratorfor that stream. EnqueueResetStreamFramepushes theRST_STREAMframe to the queue, raising the count to 10,002. This triggers the synchronous drain (10001 > 10000), invalidating the iterator.- Control returns to
ResetStreamIterator, which uses the dangling iterator, causing the UAF. The attacker can potentially reclaim the freed map node memory with a carefully crafted payload to hijack theOnClosevirtual call and achieve RCE in the Network process.
Potential Fix
Modify ResetStreamIterator to take a spdy::SpdyStreamId instead of an ActiveStreamMap::iterator. After calling EnqueueResetStreamFrame, look up the stream again using active_streams_.find(stream_id). If it is not found (because a synchronous drain occurred), simply return.
Evaluated with Chrome root at commit: a9cbf6e8b275fe4147435aa905f3b7f5a656f5f0
Results from so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.