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 <[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).
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.