High patch-now ~ plausible 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 WebRTC
DescriptionUse after free in WebRTC
ComponentWebRTC
Sub-componentBlink>WebRTC
Bug ClassUAF
Action tierpatch-now — corruption primitive, web-reachable in default config (plausible) — upheld by independent review by AI
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Independent review by AIPlausible
Reviewer notes: SendBatch hunk is not in the shown (truncated) diff, so the next-packet UAF is inferred from SendPacket previously always returning true. OnRead and OnConnected stay void (they only log), not 'converted the same way'.
A second, independent AI reviewer (not a human) re-derived this analysis from the fixing commit and the vendor advisory, looking for errors.
Tracker427681143
Fix commit0ccc849d2027 (chromium/src) +154/-46
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
Creditedjakebiles
Reported2025-06-25 (per vendor advisory)
Fix landed2025-07-02 (public git)
Disclosed2025-07-15
Patch gap13 days — fix public in source before the advisory

Background

`P2PSocketTcpBase`
the network-service class implementing WebRTC’s peer-to-peer TCP sockets, whose SendBatch sends multiple queued packets in one call.
Reentrant socket destruction
a write/read error handled via HandleWriteResult/HandleReadResult can call OnError, which tears down and deletes the P2PSocketTcpBase object mid-operation.
`SendBatch` loop
the batched-send path iterates over multiple packets, calling into the send chain (SendPacket → DoSend → WriteOrQueue → DoWrite) once per packet.
`void`-returning send chain (pre-patch)
before the fix, DoWrite, WriteOrQueue, DoSend, SendPacket, Send, DoRead, OnOpen, and InitAccepted returned void, giving callers no signal that the socket had just been destroyed.

Root Cause Analysis

Verify before building a PoC

This analysis is consistent with the fixing commit but not fully provable from it: part of the vulnerable path lives in code the fix commit does not touch. Confirm the trigger and the sink against the source before relying on it for exploit development.
What the commit does not establish: SendBatch hunk is not in the shown (truncated) diff, so the next-packet UAF is inferred from SendPacket previously always returning true. OnRead and OnConnected stay void (they only log), not 'converted the same way'.

The send and read chains returned void, so a caller had no way to learn that a downstream error had already destroyed the socket. In DoWrite, an error path (!HandleWriteResult(result)) could delete the P2PSocketTcpBase object, yet control returned normally and the caller — ultimately SendBatch processing the next packet in the batch — continued to operate on the freed this, a use-after-free.

The patch rewrites the whole chain to return bool (true = socket alive, false = socket destroyed): DoWrite now return false on write error, WriteOrQueue/DoSend/SendPacket propagate that value, Send logs and stops, and the read side (DoRead, OnRead, OnOpen, OnConnected, InitAccepted) is converted the same way. With a truthful “am I still alive” signal threaded through, callers (notably SendBatch, whose hardening is the commit’s stated purpose) can abort further processing instead of touching a deleted object. The change is a hardening/lifetime-signal propagation, not a validation tightening or relaxation.

Key insight
The core mistake was a void control-flow contract that hid reentrant self-destruction from callers, letting SendBatch keep using a freed socket; the fix propagates a bool “socket still alive” result through the entire send/read chain so callers bail out after destruction.

Attack Path

  1. Establish a P2P TCP socket A WebRTC session (attacker-influenced peer/network conditions) creates a P2PSocketTcpBase in the network service and queues a batch of packets for transmission.
  2. Send a batch where the first packet fails SendBatch sends the first packet; a write error propagates through DoWrite → HandleWriteResult → OnError, destroying the socket object, as the SendBatchWithBrokenFirstPacket regression test models.
  3. Continue the loop on freed memory Because the pre-patch chain returned void, SendBatch proceeds to process the second packet on the now-freed this, dereferencing reclaimed memory — the use-after-free. (inferred)
  4. Memory corruption Continued operations on the freed socket read/write reclaimed heap, potentially corrupting network-service state. (inferred)

Impact Assessment

An attacker able to influence WebRTC P2P TCP send/error timing can trigger a use-after-free of a P2PSocketTcpBase object in the network service process during batched sends. Successful exploitation could lead to memory corruption and potentially code execution in that process (inferred); the advisory classifies this as a use-after-free in WebRTC and does not report exploitation in the wild.

Changed Functions

FunctionChangeNotes
if
services/network/p2p/socket_tcp.cc
modified
while
services/network/p2p/socket_tcp.cc
modified

Files Changed

  • services/network/p2p/socket_tcp.cc

Audit Directions

  • Pattern
    Look for object methods that can synchronously self-destruct (via error/OnError/delete-this paths) yet return void, especially where a loop or caller continues to use this afterward.
  • Pattern
    Audit batched/looped operations (SendBatch-style) that invoke per-item callbacks capable of tearing down the owning object mid-iteration.
  • Pattern
    In the network service’s socket layers, review completion callbacks bound with base::Unretained(this) and any code that continues after HandleReadResult/HandleWriteResult without confirming the socket still exists.
From 0ccc849d2027de75122e5b187dc99bdf1cc24edf Mon Sep 17 00:00:00 2001
From: Harald Alvestrand <[email protected]>
Date: Wed, 02 Jul 2025 15:15:22 -0700
Subject: [PATCH] Harden P2PSocketTcpBase::SendBatch against errors

This change ensures that even if errors occur that cause the socket
to be deleted during packet processing, SendBatch will terminate
correctly.

Bug: 427681143
Change-Id: I41da6789c8aab44b31c00c6a7844cd86b918561c
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6694889
Reviewed-by: Sergey Ulanov <[email protected]>
Commit-Queue: Harald Alvestrand <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1481925}
---

diff --git a/services/network/p2p/socket_tcp.cc b/services/network/p2p/socket_tcp.cc
index ba68bdcf..827ea38 100644
--- a/services/network/p2p/socket_tcp.cc
+++ b/services/network/p2p/socket_tcp.cc
@@ -73,13 +73,13 @@
 
 P2PSocketTcpBase::~P2PSocketTcpBase() = default;
 
-void P2PSocketTcpBase::InitAccepted(const net::IPEndPoint& remote_address,
+bool P2PSocketTcpBase::InitAccepted(const net::IPEndPoint& remote_address,
                                     std::unique_ptr<net::StreamSocket> socket) {
   DCHECK(socket);
   remote_address_.ip_address = remote_address;
   // TODO(ronghuawu): Add FakeSSLServerSocket.
   socket_ = std::move(socket);
-  DoRead();
+  return DoRead();
 }
 
 void P2PSocketTcpBase::Init(
@@ -128,15 +128,18 @@
   DCHECK_NE(result, net::ERR_IO_PENDING);
 
   if (result != net::OK) {
-    LOG(WARNING) << "Error from connecting socket, result=" << result;
+    LOG(WARNING) << "Error from connecting socket, result=" << result
+                 << ", destroying socket";
     OnError();
     return;
   }
 
-  OnOpen();
+  if (!OnOpen()) {
+    LOG(ERROR) << "Socket destroyed in OnConnected/OnOpen";
+  }
 }
 
-void P2PSocketTcpBase::OnOpen() {
+bool P2PSocketTcpBase::OnOpen() {
   // Setting socket send and receive buffer size.
   if (net::OK != socket_->SetReceiveBufferSize(kTcpRecvSocketBufferSize)) {
     LOG(WARNING) << "Failed to set socket receive buffer size to "
@@ -149,9 +152,9 @@
   }
 
   if (!DoSendSocketCreateMsg())
-    return;
+    return false;
 
-  DoRead();
+  return DoRead();
 }
 
 bool P2PSocketTcpBase::DoSendSocketCreateMsg() {
@@ -198,7 +201,7 @@
   return true;
 }
 
-void P2PSocketTcpBase::DoRead() {
+bool P2PSocketTcpBase::DoRead() {
   while (true) {
     if (!read_buffer_.get()) {
       read_buffer_ = base::MakeRefCounted<net::GrowableIOBuffer>();
@@ -214,14 +217,23 @@
     const int result = socket_->Read(
         read_buffer_.get(), read_buffer_->RemainingCapacity(),
         base::BindOnce(&P2PSocketTcp::OnRead, base::Unretained(this)));
-    if (result == net::ERR_IO_PENDING || !HandleReadResult(result))
-      return;
+    if (result == net::ERR_IO_PENDING) {
+      return true;  // not finished, but blocked
+    }
+    if (!HandleReadResult(result)) {
+      return false;  // error, socket deleted
+    }
   }
 }
 
 void P2PSocketTcpBase::OnRead(int result) {
-  if (HandleReadResult(result))
-    DoRead();
+  if (!HandleReadResult(result)) {
+    LOG(ERROR) << "OnRead/HandleReadResult reports socket destroyed";
+    return;
+  }
+  if (!DoRead()) {
+    LOG(ERROR) << "OnRead/DoRead reports socket destroyed";
+  }
 }
 
 bool P2PSocketTcpBase::OnPacket(base::span<const uint8_t> data) {
@@ -262,17 +274,17 @@
   return true;
 }
 
-void P2PSocketTcpBase::WriteOrQueue(SendBuffer& send_buffer) {
+bool P2PSocketTcpBase::WriteOrQueue(SendBuffer& send_buffer) {
   if (write_buffer_.buffer.get()) {
     write_queue_.push(send_buffer);
-    return;
+    return true;
   }
 
   write_buffer_ = send_buffer;
-  DoWrite();
+  return DoWrite();
 }
 
-void P2PSocketTcpBase::DoWrite() {
+bool P2PSocketTcpBase::DoWrite() {
   while (!write_pending_ && write_buffer_.buffer.get()) {
     int result = socket_->Write(
         write_buffer_.buffer.get(), write_buffer_.buffer->BytesRemaining(),
@@ -282,9 +294,10 @@
     if (result == net::ERR_IO_PENDING) {
       write_pending_ = true;
     } else if (!HandleWriteResult(result)) {
-      break;
+      return false;  // Error, socket is destroyed.
     }
   }
+  return true;
 }
 
 void P2PSocketTcpBase::OnWritten(int result) {
@@ -293,8 +306,13 @@
 
   write_pending_ = false;
 
-  if (HandleWriteResult(result))
-    DoWrite();
+  if (!HandleWriteResult(result)) {
+    LOG(ERROR) << "Socket destroyed in OnWritten/HandleWriteResult";
+    return;
+  }
+  if (!DoWrite()) {
+    LOG(ERROR) << "Socket destroyed in OnWritten/DoWrite";
+  }
 }
 
 bool P2PSocketTcpBase::HandleWriteResult(int result) {
@@ -373,13 +391,14 @@
     }
   }
 
-  DoSend(packet_info.destination, data, packet_info.packet_options);
-  return true;
+  return DoSend(packet_info.destination, data, packet_info.packet_options);
 }
 
 void P2PSocketTcpBase::Send(base::span<const uint8_t> data,
                             const P2PPacketInfo& packet_info) {
-  SendPacket(data, packet_info);
+  if (!SendPacket(data, packet_info)) {
+    LOG(ERROR) << "Socket destroyed while sending";
+  }
 }
 
 void P2PSocketTcpBase::SendBatch(
@@ -444,7 +463,7 @@
   return OnPacket(input.subspan(kPacketHeaderSize, packet_size));
 }
 
-void P2PSocketTcp::DoSend(const net::IPEndPoint& to,
+bool P2PSocketTcp::DoSend(const net::IPEndPoint& to,
                           base::span<const uint8_t> data,
                           const webrtc::AsyncSocketPacketOptions& options) {
   const size_t buffer_size = kPacketHeaderSize + data.size();
@@ -468,7 +487,7 @@
                              send_buffer_without_header.size(),
                              options.packet_time_params, webrtc::TimeMicros());
 
-  WriteOrQueue(send_buffer);
+  return WriteOrQueue(send_buffer);
 }
 
 // P2PSocketStunTcp
@@ -509,7 +528,7 @@
   return OnPacket(input.first(packet_size));
 }
 
-void P2PSocketStunTcp::DoSend(const net::IPEndPoint& to,
+bool P2PSocketStunTcp::DoSend(const net::IPEndPoint& to,
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/services/network/p2p/socket_tcp_unittest.cc b/services/network/p2p/socket_tcp_unittest.cc
index fcdd217..45761c14 100644
--- a/services/network/p2p/socket_tcp_unittest.cc
+++ b/services/network/p2p/socket_tcp_unittest.cc
@@ -685,4 +685,81 @@
   EXPECT_TRUE(ssl_socket_provider.ConnectDataConsumed());
 }
 
+// Verify that we can send packets using SendBatch
+TEST_F(P2PSocketTcpTest, SendAfterStunRequestWithSendBatch) {
+  // Receive packet from |dest_|.
+  std::vector<uint8_t> request_packet;
+  CreateStunRequest(&request_packet);
+
+  std::string received_data;
+  received_data.append(IntToSize(request_packet.size()));
+  received_data.append(request_packet.begin(), request_packet.end());
+
+  EXPECT_CALL(*fake_client_.get(), SendComplete(_)).Times(2);
+
+  EXPECT_CALL(*fake_client_.get(), DataReceived(_)).Times(1);
+  EXPECT_CALL(*this, SinglePacketReceptionHelper(_, SpanEq(request_packet), _));
+  socket_->AppendInputData(received_data);
+
+  webrtc::AsyncSocketPacketOptions options;
+  // Now we should be able to send any data to |dest_|.
+  std::vector<mojom::P2PSendPacketPtr> packet_batch;
+  std::vector<uint8_t> packet;
+  // CreateRandomPacket(&packet);
+  packet = {0x01, 0x02, 0x03};
+  std::string expected_data;
+  expected_data.append(IntToSize(packet.size()));
+  expected_data.append(packet.begin(), packet.end());
+  packet_batch.emplace_back(mojom::P2PSendPacket::New(
+      packet, P2PPacketInfo(dest_.ip_address, options, 0)));
+  std::vector<uint8_t> packet2;
+  // CreateRandomPacket(&packet2);
+  packet2 = {0x04, 0x05, 0x06};
+  expected_data.append(IntToSize(packet2.size()));
+  expected_data.append(packet2.begin(), packet2.end());
+  packet_batch.emplace_back(mojom::P2PSendPacket::New(
+      packet2, P2PPacketInfo(dest_.ip_address, options, 0)));
+  socket_impl_->SendBatch(std::move(packet_batch));
+
+  EXPECT_EQ(expected_data, sent_data_);
+
+  base::RunLoop().RunUntilIdle();
+}
+
+TEST_F(P2PSocketTcpTest, SendBatchWithBrokenFirstPacket) {
+  // Receive packet from |dest_|.
+  std::vector<uint8_t> request_packet;
+  CreateStunRequest(&request_packet);
+
+  std::string received_data;
+  received_data.append(IntToSize(request_packet.size()));
+  received_data.append(request_packet.begin(), request_packet.end());
+  EXPECT_CALL(*fake_client_.get(), DataReceived(_)).Times(1);
+  EXPECT_CALL(*this, SinglePacketReceptionHelper(_, SpanEq(request_packet), _));
+
+  socket_->AppendInputData(received_data);
+
+  webrtc::AsyncSocketPacketOptions options;
+  std::vector<mojom::P2PSendPacketPtr> packet_batch;
+  std::vector<uint8_t> packet;
+  CreateRandomPacket(&packet);
+  packet_batch.emplace_back(mojom::P2PSendPacket::New(
+      packet, P2PPacketInfo(dest_.ip_address, options, 0)));
+  std::vector<uint8_t> packet2;
+  CreateRandomPacket(&packet2);
+  packet_batch.emplace_back(mojom::P2PSendPacket::New(
+      packet2, P2PPacketInfo(dest_.ip_address, options, 0)));
+  socket_->set_error_on_next_write(net::ERR_FAILED);
+  // We now expect the first packet to cause the connection to be destroyed,
+  // and the second packet to never be processed.
+  auto socket_impl_ptr = socket_impl_.get();
+  socket_delegate_.ExpectDestruction(std::move(socket_impl_));
+  // The socket will be destroyed, so clear our raw_ptr to it to avoid
+  // a dangling pointer warning.
+  socket_ = nullptr;
+  socket_impl_ptr->SendBatch(std::move(packet_batch));
+  // process any queued callbacks.
+  base::RunLoop().RunUntilIdle();
+}
+
 }  // namespace network
Loading diff…

Original Bug Report

reported by [email protected]

P2PSocket(this) object is freed, causing Use-After-Free vulnerability

Steps to reproduce the problem

Access poc.html after applying diff

Problem Description

Access to the freed object starts at: https://source.chromium.org/chromium/chromium/src/+/main:services/network/p2p/socket_tcp.cc;l=391 return true: https://source.chromium.org/chromium/chromium/src/+/main:services/network/p2p/socket_tcp.cc;l=377 The object is freed at: https://source.chromium.org/chromium/chromium/src/+/main:services/network/p2p/socket_tcp.cc;l=305

In the SendPacket function called by SendBatch, as in the first link, the iterator is stopped via return false when this object is freed.

In the third link, this object is freed, but if you look at the second link, return true is returned, so SendBatch does not detect that this object is freed, and Use-After-Free occurs.

Summary

P2PSocket(this) object is freed, causing Use-After-Free vulnerability

Custom Questions

Type of crash:

Network Service

Crash state:

Access to this object that has been freed

Reporter credit:

jakebiles

Additional Data

Category: Security
Chrome Channel: Not sure
Regression: N/A \

View on issue tracker