Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Network
DescriptionUse after free in Network
ComponentNetwork
Bug ClassUAF
Tracker497722502
Fix commitf97b03e5e419 (chromium/src) +9/-2
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-02

Files Changed

  • net/spdy/spdy_session.cc
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);
 }
 
Loading diff…

Original Bug Report

reported by [email protected]

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.cc
  • net/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:

  1. Keep at least one HTTP/2 stream active.
  2. Stall TCP reads (e.g., advertise a TCP zero-window) to stall Chrome’s outgoing socket_->Write() operations, causing outgoing frames to queue.
  3. Send exactly 10,001 PING frames (with ACK=0). Because of the strict > comparison in EnqueueSessionWrite, the 10,001st frame is queued without triggering the drain.
  4. Send a WINDOW_UPDATE frame with an invalid delta_window_size of 0 for the active stream. Chrome calls ResetStreamIterator for that stream.
  5. EnqueueResetStreamFrame pushes the RST_STREAM frame to the queue, raising the count to 10,002. This triggers the synchronous drain (10001 > 10000), invalidating the iterator.
  6. 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 the OnClose virtual 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.

View on issue tracker