High CVSS 8.8 chrome UAF 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
8.8
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
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-03-24 (per vendor advisory)
Fix landed2026-06-04 (public git)
Disclosed2026-05-19
Patch gapNot computed: several commits cite this bug and it is not yet established which one is the fix.

Background

QUIC / HTTP-3 DATAGRAM (CONNECT-UDP / MASQUE)
Chrome tunnels UDP over HTTP/3 using CONNECT-UDP; datagrams for a stream are delivered to a registered Http3DatagramVisitor.
`QuicSpdyStream::datagram_visitor_`
A raw Http3DatagramVisitor* stored on the stream (in third_party/quiche). Being in quiche, it is a native C++ raw pointer not protected by MiraclePtr/BackupRefPtr (per report; freed-memory escalation inferred).
`QuicChromiumClientStream::Handle`
Chromium’s handle wrapping a QuicSpdyStream; it registers/unregisters the datagram visitor on behalf of a consumer and holds stream_ (nulled on detach).
H3 GOAWAY / `Handle::OnError` / `Handle::Reset`
On a GOAWAY or stream error/reset, the handle detaches from its stream.

Root Cause Analysis

The lifetime invariant is that a QuicProxyDatagramClientSocket registered as a stream’s Http3DatagramVisitor must be unregistered from the underlying QuicSpdyStream before the socket is destroyed, so the stream never holds a dangling datagram_visitor_. Pre-patch, unregistration was driven solely from the socket side (QuicProxyDatagramClientSocket tracked its own datagram_visitor_registered_ and called stream_handle_->UnregisterHttp3DatagramVisitor() in its Close()/disconnect path). But QuicChromiumClientStream::Handle::OnError (and Reset) detached the handle by nulling stream_ without unregistering the visitor. After that detach, the socket’s later UnregisterHttp3DatagramVisitor() call no-opped because Handle::stream_ was already nullptr — so the still-alive QuicSpdyStream kept a raw pointer to the soon-to-be-freed socket. When a subsequent DATAGRAM arrived, QuicSpdyStream::HandleReceivedDatagram invoked datagram_visitor_->OnHttp3Datagram(...) on the freed object: a use-after-free with a virtual call (per report, quic_spdy_stream.cc:1550).

The patch moves the unregistration to the moment the handle loses its stream. Handle::OnError now calls UnregisterHttp3DatagramVisitor() before nulling stream_ (and SaveState), and Handle::Reset calls it before resetting the stream. To make this robust, registration state is tracked on the Handle itself: a new bool datagram_visitor_registered_ is set true in RegisterHttp3DatagramVisitor and gated in UnregisterHttp3DatagramVisitor (if (stream_ && datagram_visitor_registered_)), and the socket’s own duplicate datagram_visitor_registered_ flag and its unregister-in-disconnect block are removed. Now the visitor is always torn off the live stream at detach time, so the stream can never dereference a freed visitor.

Key insight
The stream handle detached on GOAWAY/error without unregistering the HTTP/3 datagram visitor, leaving QuicSpdyStream::datagram_visitor_ as a dangling raw pointer to a freed socket; the fix unregisters the visitor at detach (OnError/Reset), tracked by a handle-owned datagram_visitor_registered_ flag.

Attack Path

  1. Victim uses a malicious CONNECT-UDP/MASQUE proxy e.g. configured via WPAD on a local network. (from report)
  2. Tunnel established QuicProxyDatagramClientSocket registers itself as the stream’s Http3DatagramVisitor. (shown by RegisterHttp3DatagramVisitor(this))
  3. Force a zombie stream Proxy withholds QUIC ACKs so the stream stays in stream_map_ after close. (from report; inferred)
  4. Send H3 GOAWAY for the stream QuicChromiumClientSession::OnHttp3GoAway → Handle::OnError, which (pre-patch) nulls stream_ without unregistering the visitor. (root cause)
  5. Socket is destroyed Its Close() unregister no-ops (handle already detached), leaving datagram_visitor_ dangling on the live stream. (root cause)
  6. Reclaim + send DATAGRAM Attacker grooms the heap to reclaim the freed socket, then the proxy sends a DATAGRAM; QuicSpdyStream::HandleReceivedDatagram virtual-calls datagram_visitor_->OnHttp3Datagram on freed memory. (from report)
  7. Primitive Use-after-free with a controlled virtual call in the network process → potential RCE (escalation inferred).

Impact Assessment

Network (service) process, which is sandboxed; reachable by an attacker-controlled HTTP/3 proxy (UI:R, CVSS 8.8 H/H/H). The primitive is a use-after-free with a virtual-function call on a freed QuicProxyDatagramClientSocket; because datagram_visitor_ is a raw pointer in quiche and not MiraclePtr-protected (per report), the freed object can be reclaimed via heap grooming, giving a strong path toward control-flow hijack and RCE in the network process (escalation/freed-memory reclamation inferred, not demonstrated by the diff). Preconditions: use of a malicious CONNECT-UDP proxy and the attacker’s ability to force the zombie-stream state (withholding ACKs) and deliver a DATAGRAM after the free.

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

Audit Directions

  • Detach-without-unregister patterns
    Audit other QuicChromiumClientStream::Handle consumers (and other visitor/observer registrations keyed to a stream) for teardown paths that null stream_ without first unregistering callbacks.
  • Zombie/waiting-for-ACK streams
    Review QuicSession::GetActiveStream/stream_map_ handling to ensure streams retained for pending ACKs cannot deliver callbacks to detached consumers.
  • Raw visitor pointers in quiche
    Since datagram_visitor_ and similar pointers are outside MiraclePtr, inventory raw visitor/observer pointers in third_party/quiche that outlive their registrants for the same UAF shape.
  • GOAWAY/error fan-out
    Review OnHttp3GoAway/OnError fan-out across stream types (WebTransport, CONNECT-UDP) for other visitors that must be unregistered on error.
From 8dbb4871d844752cd7b55e25dad51ad6cf12185e Mon Sep 17 00:00:00 2001
From: David Schinazi <[email protected]>
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 <[email protected]>
Auto-Submit: David Schinazi <[email protected]>
Commit-Queue: Nidhi Jaju <[email protected]>
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 [email protected]

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