CVE-2026-9114
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifnet/quic/quic_chromium_client_stream.cc |
modified | |
ifnet/quic/quic_proxy_datagram_client_socket.cc |
modified |
Files Changed
net/quic/quic_chromium_client_stream.ccnet/quic/quic_chromium_client_stream.hnet/quic/quic_proxy_datagram_client_socket.ccnet/quic/quic_proxy_datagram_client_socket.h
Patch
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).
Original Bug Report
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.ccnet/quic/quic_proxy_datagram_client_socket.ccnet/third_party/quiche/src/quiche/quic/core/http/quic_spdy_stream.ccnet/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
-
Visitor Registration:
QuicProxyDatagramClientSocketregisters itself as anHttp3DatagramVisitoron an underlyingQuicSpdyStreamusing aQuicChromiumClientStream::Handle. This registration stores a raw pointer to the socket inQuicSpdyStream::datagram_visitor_(defined innet/third_party/quiche/src/quiche/quic/core/http/quic_spdy_stream.h). -
H3 GOAWAY Processing: When a proxy sends an HTTP/3 GOAWAY frame,
QuicChromiumClientSession::OnHttp3GoAwayis triggered. This function callsOnErroron all active streams affected by the GOAWAY. For a CONNECT-UDP stream,QuicChromiumClientStream::OnErroris called. -
Handle Detachment:
QuicChromiumClientStream::OnErrordetaches itsHandleby settinghandle_->stream_ = nullptr. Crucially, the underlyingQuicSpdyStreamremains active in the session’s stream map and does not reset itsdatagram_visitor_pointer. -
Unregistration Failure: When the
QuicProxyDatagramClientSocketis eventually destroyed (e.g., as the inner session is torn down), itsClose()method attempts to unregister the visitor. This call goes throughHandle::UnregisterHttp3DatagramVisitor(). Because theHandlewas already detached from the stream in step 3, the unregistration function silently no-ops:void QuicChromiumClientStream::Handle::UnregisterHttp3DatagramVisitor() { if (stream_) { stream_->UnregisterHttp3DatagramVisitor(); // Silently skipped } } -
Use-After-Free: The
QuicSpdyStreamcontinues 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’sstream_map_as a “zombie stream”.QuicSession::GetActiveStreamdoes not filter these streams. -
Triggering the Dereference: If the proxy subsequently sends a
DATAGRAMframe for the stream ID,QuicSpdyStream::OnDatagramReceivedis called, which invokesHandleReceivedDatagram. This function blindly callsdatagram_visitor_->OnHttp3Datagram(...)(located atnet/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.)
- Configure Chrome to use a malicious CONNECT-UDP (MASQUE) proxy (e.g., via WPAD).
- A web application establishes a CONNECT-UDP tunnel. The proxy responds with 200 headers (setting
headers_decompressed_=trueon the outerQuicSpdyStream). - 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.
- The proxy sends an HTTP/3 GOAWAY frame on the outer session’s control stream that includes the CONNECT-UDP stream’s ID.
- Chrome processes the GOAWAY, nullifying the stream handle but leaving the underlying stream and its dangling visitor pointer alive in the session.
- The inner session is destroyed due to the network error, freeing the
QuicProxyDatagramClientSocket. - The attacker proxy uses standard heap grooming to overwrite the freed socket object, pointing its vtable to attacker-controlled memory.
- The proxy sends a QUIC
DATAGRAMframe for the outer CONNECT-UDP stream’s ID. - 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.