Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in QUIC
DescriptionUse after free in QUIC
ComponentQUIC
Bug ClassUAF
Tracker495798630
Fix commit8dbb4871d844 (chromium/src) +7/-9
CISA KEVNot listed
CreditedGoogle
Disclosed2026-05-19

Changed Functions

FunctionChangeNotes
if
net/quic/quic_chromium_client_stream.cc
modified
if
net/quic/quic_proxy_datagram_client_socket.cc
modified

Files Changed

  • net/quic/quic_chromium_client_stream.cc
  • net/quic/quic_chromium_client_stream.h
  • net/quic/quic_proxy_datagram_client_socket.cc
  • net/quic/quic_proxy_datagram_client_socket.h
From 8dbb4871d844752cd7b55e25dad51ad6cf12185e Mon Sep 17 00:00:00 2001
From: David Schinazi <dschinazi@chromium.org>
Date: Thu, 04 Jun 2026 01:19:56 -0700
Subject: [PATCH] Unregister Http3DatagramVisitor on stream handle error.

When a QuicChromiumClientStream::Handle encounters an error, unregister any associated Http3DatagramVisitor to prevent potential issues after the stream is destroyed.

Bug: 495798630
Change-Id: Ibc66d5e14ec33114ad4fed15f62f482d42984760
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7900197
Reviewed-by: Nidhi Jaju <nidhijaju@chromium.org>
Auto-Submit: David Schinazi <dschinazi@chromium.org>
Commit-Queue: Nidhi Jaju <nidhijaju@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1641523}
---

diff --git a/net/quic/quic_chromium_client_stream.cc b/net/quic/quic_chromium_client_stream.cc
index 0aec8789..61a241579 100644
--- a/net/quic/quic_chromium_client_stream.cc
+++ b/net/quic/quic_chromium_client_stream.cc
@@ -156,6 +156,7 @@
 
 void QuicChromiumClientStream::Handle::OnError(int error) {
   net_error_ = error;
+  UnregisterHttp3DatagramVisitor();
   if (stream_)
     SaveState();
   stream_ = nullptr;
@@ -372,6 +373,7 @@
 
 void QuicChromiumClientStream::Handle::Reset(
     quic::QuicRstStreamErrorCode error_code) {
+  UnregisterHttp3DatagramVisitor();
   if (stream_)
     stream_->Reset(error_code);
 }
@@ -380,12 +382,14 @@
     Http3DatagramVisitor* visitor) {
   if (stream_) {
     stream_->RegisterHttp3DatagramVisitor(visitor);
+    datagram_visitor_registered_ = true;
   }
 }
 
 void QuicChromiumClientStream::Handle::UnregisterHttp3DatagramVisitor() {
-  if (stream_) {
+  if (stream_ && datagram_visitor_registered_) {
     stream_->UnregisterHttp3DatagramVisitor();
+    datagram_visitor_registered_ = false;
   }
 }
 
diff --git a/net/quic/quic_chromium_client_stream.h b/net/quic/quic_chromium_client_stream.h
index 7365727..4f6d4756 100644
--- a/net/quic/quic_chromium_client_stream.h
+++ b/net/quic/quic_chromium_client_stream.h
@@ -229,6 +229,8 @@
     uint64_t ietf_application_error_ = 0;
     bool fin_sent_;
     bool fin_received_;
+    // Visitor on stream is registered to receive HTTP/3 datagrams.
+    bool datagram_visitor_registered_ = false;
     uint64_t stream_bytes_read_;
     uint64_t stream_bytes_written_;
     bool is_done_reading_;
diff --git a/net/quic/quic_proxy_datagram_client_socket.cc b/net/quic/quic_proxy_datagram_client_socket.cc
index 32cec74..d29ada5 100644
--- a/net/quic/quic_proxy_datagram_client_socket.cc
+++ b/net/quic/quic_proxy_datagram_client_socket.cc
@@ -80,7 +80,6 @@
 
   // Register stream to receive HTTP/3 datagrams.
   stream_handle_->RegisterHttp3DatagramVisitor(this);
-  datagram_visitor_registered_ = true;
 
   DCHECK_EQ(STATE_DISCONNECTED, next_state_);
   next_state_ = STATE_CALCULATE_HEADERS;
@@ -134,11 +133,6 @@
 
   next_state_ = STATE_DISCONNECTED;
 
-  if (datagram_visitor_registered_) {
-    stream_handle_->UnregisterHttp3DatagramVisitor();
-    datagram_visitor_registered_ = false;
-  }
-
   connect_request_sent_ = false;
   awaiting_connect_response_ = false;
 
diff --git a/net/quic/quic_proxy_datagram_client_socket.h b/net/quic/quic_proxy_datagram_client_socket.h
index 8afaa11..25680119 100644
--- a/net/quic/quic_proxy_datagram_client_socket.h
+++ b/net/quic/quic_proxy_datagram_client_socket.h
@@ -194,8 +194,6 @@
   // a buffer, allowing datagrams to be stored when received and processed
   // asynchronously at a later time.
   std::queue<std::string> datagrams_;
-  // Visitor on stream is registered to receive HTTP/3 datagrams.
-  bool datagram_visitor_registered_ = false;
 
   // Tracks whether the CONNECT-UDP request has been sent (even if response not
   // received yet).
Loading diff…

Original Bug Report

reported by vm...@google.com

Use-After-Free in QuicSpdyStream::datagram_visitor_ during H3 GOAWAY

Project Fortify, an experimental security project, has identified the following potential security issue.

Overview: A Use-After-Free exists in the network process when an HTTP/3 GOAWAY frame causes a stream handle to detach without unregistering its datagram visitor. This leaves a dangling, unprotected raw pointer in QuicSpdyStream that is dereferenced when a subsequent DATAGRAM frame is received, potentially allowing for remote code execution.

Affected files:

  • net/quic/quic_chromium_client_stream.cc
  • net/quic/quic_proxy_datagram_client_socket.cc
  • net/third_party/quiche/src/quiche/quic/core/http/quic_spdy_stream.cc
  • net/quic/quic_chromium_client_session.cc

Estimated timestamp from git blame: 2025-10-08

Description

A potential Use-After-Free (UAF) vulnerability exists in the Chrome network process when using CONNECT-UDP (MASQUE) or IP Protection proxies. The issue arises from a lifecycle mismatch between QuicProxyDatagramClientSocket and QuicSpdyStream when an HTTP/3 GOAWAY frame is received.

Technical Details

  1. Visitor Registration: QuicProxyDatagramClientSocket registers itself as an Http3DatagramVisitor on an underlying QuicSpdyStream using a QuicChromiumClientStream::Handle. This registration stores a raw pointer to the socket in QuicSpdyStream::datagram_visitor_ (defined in net/third_party/quiche/src/quiche/quic/core/http/quic_spdy_stream.h).

  2. H3 GOAWAY Processing: When a proxy sends an HTTP/3 GOAWAY frame, QuicChromiumClientSession::OnHttp3GoAway is triggered. This function calls OnError on all active streams affected by the GOAWAY. For a CONNECT-UDP stream, QuicChromiumClientStream::OnError is called.

  3. Handle Detachment: QuicChromiumClientStream::OnError detaches its Handle by setting handle_->stream_ = nullptr. Crucially, the underlying QuicSpdyStream remains active in the session’s stream map and does not reset its datagram_visitor_ pointer.

  4. Unregistration Failure: When the QuicProxyDatagramClientSocket is eventually destroyed (e.g., as the inner session is torn down), its Close() method attempts to unregister the visitor. This call goes through Handle::UnregisterHttp3DatagramVisitor(). Because the Handle was already detached from the stream in step 3, the unregistration function silently no-ops:

    void QuicChromiumClientStream::Handle::UnregisterHttp3DatagramVisitor() {
      if (stream_) {
        stream_->UnregisterHttp3DatagramVisitor(); // Silently skipped
      }
    }
    
  5. Use-After-Free: The QuicSpdyStream continues to hold a dangling raw pointer to the deleted socket. A secondary trigger mechanism guarantees the stream stays alive: if a stream is closed while still waiting for ACKs (which an attacker can easily enforce by withholding ACKs), it remains in the session’s stream_map_ as a “zombie stream”. QuicSession::GetActiveStream does not filter these streams.

  6. Triggering the Dereference: If the proxy subsequently sends a DATAGRAM frame for the stream ID, QuicSpdyStream::OnDatagramReceived is called, which invokes HandleReceivedDatagram. This function blindly calls datagram_visitor_->OnHttp3Datagram(...) (located at net/third_party/quiche/src/quiche/quic/core/http/quic_spdy_stream.cc:1550), resulting in a virtual function call on a freed object.

Impact

This vulnerability allows for potential Remote Code Execution (RCE) in the sandboxed Network Process. Because the datagram_visitor_ pointer resides in the third_party/quiche directory, it is a native C++ raw pointer (Http3DatagramVisitor*) and is not protected by MiraclePtr (BackupRefPtr). An attacker-controlled proxy (such as one configured via WPAD on a local network) can use this to execute arbitrary code by grooming the heap and reclaiming the freed socket’s memory prior to sending the DATAGRAM frame.

Potential Reproduction Steps

(Note: These are potential steps as a full PoC has not yet been developed.)

  1. Configure Chrome to use a malicious CONNECT-UDP (MASQUE) proxy (e.g., via WPAD).
  2. A web application establishes a CONNECT-UDP tunnel. The proxy responds with 200 headers (setting headers_decompressed_=true on the outer QuicSpdyStream).
  3. The attacker proxy intentionally withholds QUIC ACKs for data sent by the browser on the CONNECT-UDP stream, ensuring the stream enters a “zombie stream” state when closed.
  4. The proxy sends an HTTP/3 GOAWAY frame on the outer session’s control stream that includes the CONNECT-UDP stream’s ID.
  5. Chrome processes the GOAWAY, nullifying the stream handle but leaving the underlying stream and its dangling visitor pointer alive in the session.
  6. The inner session is destroyed due to the network error, freeing the QuicProxyDatagramClientSocket.
  7. The attacker proxy uses standard heap grooming to overwrite the freed socket object, pointing its vtable to attacker-controlled memory.
  8. The proxy sends a QUIC DATAGRAM frame for the outer CONNECT-UDP stream’s ID.
  9. The browser’s network process accesses the dangling datagram_visitor_ pointer and invokes a virtual function, hijacking the instruction pointer and leading to RCE.

Evaluated with Chrome root at commit: 0eb4855bda702feaaa8b899336664f97e3df88b8


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. Please feel free to reach out to me if you have concerns or feedback.

View on issue tracker