Chrome · WebSockets
CVE-2026-17947
UAF in WebSockets
Overview
Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifnet/websockets/websocket_basic_stream_adapters.cc |
modified |
Files Changed
net/websockets/websocket_basic_stream_adapters.ccnet/websockets/websocket_basic_stream_adapters.hnet/websockets/websocket_basic_stream_adapters_test.cc
Patch
From 2d7f3c0fb08fd5c5905c39acd44adf4a6f8ca763 Mon Sep 17 00:00:00 2001
From: Mayur Patil <patilmayur@microsoft.com>
Date: Tue, 09 Jun 2026 08:17:40 -0700
Subject: [PATCH] [WebSocket/H3] Handle QUIC stream close during write
WebSocketQuicStreamAdapter::Write() can trigger a QUIC stream close
while WriteOrBufferBody() is running. If the close path clears or
destroys the adapter, Write() must not continue using the old stream
pointer after the call returns.
Bug: 515438256
Change-Id: Id02d2c75a99543d868c438dcbd29065644062449
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7874191
Reviewed-by: Nidhi Jaju <nidhijaju@chromium.org>
Commit-Queue: Mayur Patil <patilmayur@microsoft.com>
Reviewed-by: Adam Rice <ricea@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1643981}
---
diff --git a/net/websockets/websocket_basic_stream_adapters.cc b/net/websockets/websocket_basic_stream_adapters.cc
index 12e769bb..707b04a 100644
--- a/net/websockets/websocket_basic_stream_adapters.cc
+++ b/net/websockets/websocket_basic_stream_adapters.cc
@@ -284,7 +284,7 @@
int buf_len,
CompletionOnceCallback callback) {
if (!websocket_quic_spdy_stream_) {
- return ERR_UNEXPECTED;
+ return stream_error_;
}
int rv = websocket_quic_spdy_stream_->Read(buf, buf_len);
@@ -304,15 +304,29 @@
CompletionOnceCallback callback,
const NetworkTrafficAnnotationTag& traffic_annotation) {
DCHECK(!write_callback_);
- DCHECK(websocket_quic_spdy_stream_);
CHECK_GT(buf_len, 0);
DCHECK(callback);
+ if (!websocket_quic_spdy_stream_) {
+ return stream_error_;
+ }
+
// Queue data to the QUIC stream. WriteOrBufferBody() either sends the data
// immediately if flow control allows, or buffers it internally.
+ // It can also synchronously close the connection on socket write errors.
+ base::WeakPtr<WebSocketQuicStreamAdapter> weak_this =
+ weak_factory_.GetWeakPtr();
websocket_quic_spdy_stream_->WriteOrBufferBody(
{buf->data(), static_cast<size_t>(buf_len)},
/*fin=*/false);
+ // If the adapter was destroyed by a callback during the write, return
+ // safely without accessing member variables.
+ if (!weak_this) {
+ return ERR_CONNECTION_CLOSED;
+ }
+ if (!websocket_quic_spdy_stream_) {
+ return stream_error_;
+ }
// Check CanWriteNewData() after queuing rather than before. This is necessary
// because WriteOrBufferBody() may have caused the send buffer to cross its
@@ -403,21 +417,27 @@
void WebSocketQuicStreamAdapter::OnClose(int status) {
CHECK_LE(status, 0);
- auto self = weak_factory_.GetWeakPtr();
- ClearStream();
-
if (status == OK) {
status = ERR_CONNECTION_CLOSED;
}
+ stream_error_ = status;
+
+ base::WeakPtr<WebSocketQuicStreamAdapter> weak_this =
+ weak_factory_.GetWeakPtr();
+ ClearStream();
+
+ // Running a completion callback can delete the current
+ // WebSocketQuicStreamAdapter. In that case, `weak_this` becomes invalid and
+ // OnClose() must return before accessing more member variables.
if (read_callback_) {
std::move(read_callback_).Run(status);
- if (!self) { // |this| might have been destroyed.
+ if (!weak_this) {
return;
}
}
if (write_callback_) {
std::move(write_callback_).Run(status);
- if (!self) { // |this| might have been destroyed.
+ if (!weak_this) {
return;
}
}
diff --git a/net/websockets/websocket_basic_stream_adapters.h b/net/websockets/websocket_basic_stream_adapters.h
index 8dfc9d5..0fa7882 100644
--- a/net/websockets/websocket_basic_stream_adapters.h
+++ b/net/websockets/websocket_basic_stream_adapters.h
@@ -239,6 +239,9 @@
// because they may be destroyed in any order.
raw_ptr<WebSocketQuicSpdyStream> websocket_quic_spdy_stream_;
+ // Close error returned by Read() and Write() after the stream is cleared.
+ int stream_error_ = ERR_UNEXPECTED;
+
raw_ptr<Delegate> delegate_;
// Read buffer, length and callback used for asynchronous read operations.
diff --git a/net/websockets/websocket_basic_stream_adapters_test.cc b/net/websockets/websocket_basic_stream_adapters_test.cc
index b0929d0..d084be6 100644
--- a/net/websockets/websocket_basic_stream_adapters_test.cc
+++ b/net/websockets/websocket_basic_stream_adapters_test.cc
@@ -2227,6 +2227,57 @@
EXPECT_TRUE(mock_quic_data_.AllWriteDataConsumed());
}
+// Verifies that WebSocketQuicStreamAdapter::Write() safely returns when
+// QuicSpdyStream::WriteOrBufferBody() synchronously closes the QUIC stream and
+// a pending read callback deletes the adapter before Write() continues.
+TEST_P(WebSocketQuicStreamAdapterTest,
+ WriteSyncSocketErrorDestroysAdapterWithPendingRead) {
+ int packet_number = 1;
+
+ mock_quic_data_.AddWrite(SYNCHRONOUS,
+ ConstructSettingsPacket(packet_number++));
+ mock_quic_data_.AddWrite(
+ SYNCHRONOUS, client_maker_.MakeRequestHeadersPacket(
+ packet_number++, client_data_stream_id1_, /*fin=*/false,
+ ConvertRequestPriorityToQuicPriority(LOWEST),
+ RequestHeaders(), nullptr));
+ // This write is the DATA packet from WebSocketQuicStreamAdapter::Write().
+ // Fail it synchronously to close the connection inside WriteOrBufferBody().
+ mock_quic_data_.AddWrite(SYNCHRONOUS, ERR_CONNECTION_REFUSED);
+
+ Initialize();
+
+ net::QuicChromiumClientSession::Handle* session_handle =
+ GetQuicSessionHandle();
+ ASSERT_TRUE(session_handle);
+
+ TestWebSocketQuicStreamAdapterCompletionCallback creation_callback;
+ auto adapter = session_handle->CreateWebSocketQuicStreamAdapter(
+ &mock_delegate_, creation_callback.callback(),
+ TRAFFIC_ANNOTATION_FOR_TESTS);
+ ASSERT_TRUE(adapter);
+ adapter->WriteHeaders(RequestHeaders(), false);
+
+ // Start a Read() that will finish later. DeleterCallback owns the adapter and
+ // deletes it when the read callback runs.
+ DeleterCallback callback(std::move(adapter));
+ constexpr int kReadBufSize = 1024;
+ auto read_buf = base::MakeRefCounted<IOBufferWithSize>(kReadBufSize);
+ int rv = callback.adapter()->Read(read_buf.get(), kReadBufSize,
+ callback.callback());
+ EXPECT_THAT(rv, IsError(ERR_IO_PENDING));
+
+ // This WebSocketQuicStreamAdapter::Write() triggers the mocked socket error.
+ // Closing the stream runs the pending read callback, which deletes the
+ // adapter. Write() must return without using the deleted adapter.
+ auto write_buf = base::MakeRefCounted<StringIOBuffer>("test data");
+ rv = callback.adapter()->Write(write_buf.get(), write_buf->size(),
+ base::DoNothing(),
+ TRAFFIC_ANNOTATION_FOR_TESTS);
+ EXPECT_THAT(rv, IsError(ERR_CONNECTION_CLOSED));
+ EXPECT_THAT(callback.WaitForResult(), IsError(ERR_QUIC_PROTOCOL_ERROR));
+}
+
// Tests that the adapter correctly handles being destroyed from within its own
// OnClose() delegate method, when there are no pending read or write
// callbacks.
Loading diff…
Regression Test / PoC
shipped with the fix
diff --git a/net/websockets/websocket_basic_stream_adapters_test.cc b/net/websockets/websocket_basic_stream_adapters_test.cc
index b0929d0..d084be6 100644
--- a/net/websockets/websocket_basic_stream_adapters_test.cc
+++ b/net/websockets/websocket_basic_stream_adapters_test.cc
@@ -2227,6 +2227,57 @@
EXPECT_TRUE(mock_quic_data_.AllWriteDataConsumed());
}
+// Verifies that WebSocketQuicStreamAdapter::Write() safely returns when
+// QuicSpdyStream::WriteOrBufferBody() synchronously closes the QUIC stream and
+// a pending read callback deletes the adapter before Write() continues.
+TEST_P(WebSocketQuicStreamAdapterTest,
+ WriteSyncSocketErrorDestroysAdapterWithPendingRead) {
+ int packet_number = 1;
+
+ mock_quic_data_.AddWrite(SYNCHRONOUS,
+ ConstructSettingsPacket(packet_number++));
+ mock_quic_data_.AddWrite(
+ SYNCHRONOUS, client_maker_.MakeRequestHeadersPacket(
+ packet_number++, client_data_stream_id1_, /*fin=*/false,
+ ConvertRequestPriorityToQuicPriority(LOWEST),
+ RequestHeaders(), nullptr));
+ // This write is the DATA packet from WebSocketQuicStreamAdapter::Write().
+ // Fail it synchronously to close the connection inside WriteOrBufferBody().
+ mock_quic_data_.AddWrite(SYNCHRONOUS, ERR_CONNECTION_REFUSED);
+
+ Initialize();
+
+ net::QuicChromiumClientSession::Handle* session_handle =
+ GetQuicSessionHandle();
+ ASSERT_TRUE(session_handle);
+
+ TestWebSocketQuicStreamAdapterCompletionCallback creation_callback;
+ auto adapter = session_handle->CreateWebSocketQuicStreamAdapter(
+ &mock_delegate_, creation_callback.callback(),
+ TRAFFIC_ANNOTATION_FOR_TESTS);
+ ASSERT_TRUE(adapter);
+ adapter->WriteHeaders(RequestHeaders(), false);
+
+ // Start a Read() that will finish later. DeleterCallback owns the adapter and
+ // deletes it when the read callback runs.
+ DeleterCallback callback(std::move(adapter));
+ constexpr int kReadBufSize = 1024;
+ auto read_buf = base::MakeRefCounted<IOBufferWithSize>(kReadBufSize);
+ int rv = callback.adapter()->Read(read_buf.get(), kReadBufSize,
+ callback.callback());
+ EXPECT_THAT(rv, IsError(ERR_IO_PENDING));
+
+ // This WebSocketQuicStreamAdapter::Write() triggers the mocked socket error.
+ // Closing the stream runs the pending read callback, which deletes the
+ // adapter. Write() must return without using the deleted adapter.
+ auto write_buf = base::MakeRefCounted<StringIOBuffer>("test data");
+ rv = callback.adapter()->Write(write_buf.get(), write_buf->size(),
+ base::DoNothing(),
+ TRAFFIC_ANNOTATION_FOR_TESTS);
+ EXPECT_THAT(rv, IsError(ERR_CONNECTION_CLOSED));
+ EXPECT_THAT(callback.WaitForResult(), IsError(ERR_QUIC_PROTOCOL_ERROR));
+}
+
// Tests that the adapter correctly handles being destroyed from within its own
// OnClose() delegate method, when there are no pending read or write
// callbacks.
Loading diff…
Original Bug Report
The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.
References
On This Page