CVE-2025-7657
Overview
Background
- `P2PSocketTcpBase`
- the network-service class implementing WebRTC’s peer-to-peer TCP sockets, whose
SendBatchsends multiple queued packets in one call. - Reentrant socket destruction
- a write/read error handled via
HandleWriteResult/HandleReadResultcan callOnError, which tears down and deletes theP2PSocketTcpBaseobject 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, andInitAcceptedreturnedvoid, giving callers no signal that the socket had just been destroyed.
Root Cause Analysis
Verify before building a PoC
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.
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
- Establish a P2P TCP socket
A WebRTC session (attacker-influenced peer/network conditions) creates a
P2PSocketTcpBasein the network service and queues a batch of packets for transmission. - Send a batch where the first packet fails
SendBatchsends the first packet; a write error propagates throughDoWrite→HandleWriteResult→OnError, destroying the socket object, as theSendBatchWithBrokenFirstPacketregression test models. - Continue the loop on freed memory
Because the pre-patch chain returned
void,SendBatchproceeds to process the second packet on the now-freedthis, dereferencing reclaimed memory — the use-after-free. (inferred) - Memory corruption Continued operations on the freed socket read/write reclaimed heap, potentially corrupting network-service state. (inferred)
Impact Assessment
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
| Function | Change | Notes |
|---|---|---|
ifservices/network/p2p/socket_tcp.cc |
modified | |
whileservices/network/p2p/socket_tcp.cc |
modified |
Files Changed
services/network/p2p/socket_tcp.cc
Audit Directions
- PatternLook for object methods that can synchronously self-destruct (via error/
OnError/delete-this paths) yet returnvoid, especially where a loop or caller continues to usethisafterward. - PatternAudit batched/looped operations (
SendBatch-style) that invoke per-item callbacks capable of tearing down the owning object mid-iteration. - PatternIn the network service’s socket layers, review completion callbacks bound with
base::Unretained(this)and any code that continues afterHandleReadResult/HandleWriteResultwithout confirming the socket still exists.
Patch
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,
Regression Test / PoC
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
Original Bug Report
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 \