Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in WebSockets
DescriptionUse after free in WebSockets
ComponentWebSockets
Bug ClassUAF
Tracker499194333
Fix commit5d8f582ac89f (chromium/src) +6/-2
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-02

Changed Functions

FunctionChangeNotes
if
net/websockets/websocket_basic_stream_adapters.cc
modified

Files Changed

  • net/websockets/websocket_basic_stream_adapters.cc
From 5d8f582ac89fd617c87bde4d0f7607743e39c8e0 Mon Sep 17 00:00:00 2001
From: Andrew Paseltiner <[email protected]>
Date: Wed, 08 Apr 2026 04:39:08 -0700
Subject: [PATCH] WebSocket: Fix sequencing-related UAF in WebSocketSpdyStreamAdapter

Evaluate the result of CopySavedReadDataIntoBuffer() before moving and
running read_callback_ to avoid a Use-After-Free vulnerability due to
C++17 sequencing rules. We also move the callback into a local variable
for additional safety.

Fixed: 499194333
Change-Id: I67e80f5f1f6ffc89b8e0f0af585e5b22e7cea570
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7726993
Auto-Submit: Andrew Paseltiner <[email protected]>
Reviewed-by: Adam Rice <[email protected]>
Commit-Queue: Adam Rice <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1611387}
---

diff --git a/net/websockets/websocket_basic_stream_adapters.cc b/net/websockets/websocket_basic_stream_adapters.cc
index 8f3fb3e..a1e691d 100644
--- a/net/websockets/websocket_basic_stream_adapters.cc
+++ b/net/websockets/websocket_basic_stream_adapters.cc
@@ -165,8 +165,12 @@
   }
 
   read_data_.Enqueue(std::move(buffer));
-  if (read_callback_)
-    std::move(read_callback_).Run(CopySavedReadDataIntoBuffer());
+  if (read_callback_) {
+    // Avoid UAF due to C++17 sequencing rules. See crbug.com/499194333.
+    auto callback = std::move(read_callback_);
+    int rv = CopySavedReadDataIntoBuffer();
+    std::move(callback).Run(rv);
+  }
 }
 
 void WebSocketSpdyStreamAdapter::OnDataSent() {
Loading diff…

Original Bug Report

reported by [email protected]

Use-After-Free in WebSocketSpdyStreamAdapter via sequence evaluation reentrancy

Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports without the security team.

Overview: A potential Use-After-Free vulnerability exists in WebSocketSpdyStreamAdapter due to C++17 evaluation order sequencing combined with reentrant stream destruction. An attacker can manipulate a HTTP/2 WebSocket connection to trigger synchronous adapter deletion during a read operation, bypassing MiraclePtr protection and potentially achieving Remote Code Execution in the Network process.

Affected files:

  • net/websockets/websocket_basic_stream_adapters.cc
  • net/spdy/spdy_read_queue.cc
  • net/spdy/spdy_buffer.cc
  • net/spdy/spdy_session.cc
  • services/network/websocket.cc

Estimated timestamp from git blame: 2019-03-11

Summary

A potential Use-After-Free (UAF) vulnerability exists in WebSocketSpdyStreamAdapter. The issue stems from a C++17 sequence evaluation trap during callback execution, coupled with a highly specific reentrant stream teardown sequence. A malicious server can intentionally exhaust the SPDY session’s capped frame queue to trigger synchronous connection drainage during an active read, destroying the adapter while its methods are still on the stack.

Technical Details

In net/websockets/websocket_basic_stream_adapters.cc, the OnDataReceived method executes the following:

if (read_callback_)
  std::move(read_callback_).Run(CopySavedReadDataIntoBuffer());

Per C++17 sequencing rules ([expr.call]/8), the postfix-expression (the object the method is called on) is evaluated before the argument expression. Therefore, std::move(read_callback_) is evaluated first, caching the memory address of the read_callback_ member variable as the implicit this pointer for the upcoming Run() call.

Next, the argument CopySavedReadDataIntoBuffer() is evaluated, which initiates a chain of events that can synchronously destroy the adapter:

  1. CopySavedReadDataIntoBuffer calls read_data_.Dequeue(), which extracts data from a SpdyBuffer and pops it from the circular_deque, destroying the buffer.
  2. The ~SpdyBuffer destructor fires, triggering ConsumeHelper(DISCARD), which calls the registered SpdySession::OnReadBufferConsumed callback.
  3. This callback increments the session’s receive window. If conditions are met (e.g., >5 seconds elapsed), it calls SendWindowUpdateFrame, enqueuing a WINDOW_UPDATE frame to the session.
  4. WINDOW_UPDATE frames are “capped frames”. If the session’s write queue is already full of capped frames (e.g., due to an attacker-induced PING flood), EnqueueSessionWrite aborts and calls DoDrainSession to close the connection.
  5. DoDrainSession iterates through active streams and calls SpdyStream::OnClose.

MiraclePtr Bypass: Inside SpdyStream::OnClose, the code copies the delegate_ (which is a raw_ptr) to a local stack pointer and then nullifies the raw_ptr before invoking the closure:

Delegate* delegate = delegate_;
delegate_ = nullptr;
if (delegate)
  delegate->OnClose(status);

Because the raw_ptr is cleared before the delegate is destroyed, MiraclePtr (BackupRefPtr) drops its quarantine protection on the adapter’s memory.

The stack continues through WebSocketSpdyStreamAdapter::OnClose, which executes the un-consumed read_callback_, tearing down the WebSocketChannel and synchronously freeing the adapter.

Finally, the stack unwinds back to the original std::move(read_callback_).Run(...) call in OnDataReceived. The execution proceeds using the dangling this pointer cached earlier. Since the memory is unprotected, an attacker who has sprayed the heap can control the BindState and its polymorphic_invoke_ function pointer, leading to Remote Code Execution.

Potential Attacker Steps

(Note: These are suggested steps to trigger the flaw; our agent does not have the ability to run active exploit code)

  1. Establish Connection: Serve a malicious webpage that opens a WebSocket connection (wss://) over HTTP/2 to an attacker-controlled server.
  2. Stall Output: The server artificially sets its TCP receive window to 0, preventing Chrome from flushing its write queue to the network.
  3. Queue Saturation: The server sends exactly 10,000 SPDY/HTTP2 PING frames. Chrome queues 10,000 PING ACK frames, perfectly hitting the session_max_queued_capped_frames_ limit.
  4. Wait: The server waits slightly more than 5 seconds (the default time_to_buffer_small_window_updates_ threshold).
  5. Trigger: The server sends a short WebSocket data frame. The processing of this frame destroys the consumed SpdyBuffer, triggering the window update, exceeding the capped frame limit, causing session drainage, destroying the adapter, and eventually triggering the UAF when the outer callback Run() is invoked.

Suggested Fix

Break the sequencing trap by evaluating the argument and storing the result in a local variable before moving and running the callback:

// In net/websockets/websocket_basic_stream_adapters.cc: WebSocketSpdyStreamAdapter::OnDataReceived

if (read_callback_) {
  int rv = CopySavedReadDataIntoBuffer();
  std::move(read_callback_).Run(rv);
}

Evaluated with Chrome root at commit: ff3d2b74fa39431785bd60e51463b08fcc71ee33


Results 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