Overview

Low
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Network
DescriptionUse after free in Network
ComponentNetwork
Bug ClassUAF
Tracker497989379
Fix commite3f7c8d6970c (chromium/src) +191/-1
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-02

Changed Functions

FunctionChangeNotes
MojoToNetPendingBufferOwningIOBuffer
services/network/socket_data_pump.cc
modified
PumpDestroyingDelegate
services/network/socket_data_pump_unittest.cc
modified
if
services/network/socket_data_pump_unittest.cc
modified
BlockedStreamSocket
services/network/socket_data_pump_unittest.cc
modified

Files Changed

  • services/network/socket_data_pump.cc
  • services/network/socket_data_pump_unittest.cc
From e3f7c8d6970c9727dbd5d94af51be42c126ae51f Mon Sep 17 00:00:00 2001
From: Adam Rice <ricea@chromium.org>
Date: Wed, 08 Apr 2026 03:36:12 -0700
Subject: [PATCH] SocketDataPump: Don't destroy the data pipe while a write is pending

network::SocketDataPump had an issue where if it shut down while a
network write was pending the contents of the mojo data pipe would be
read from after it was freed.

To prevent this, keep the handle on the mojo data pipe alive while the
IOBuffer exists. Also add a test.

Fixed: 497989379
Change-Id: I43fcb26fd8f9f02f587a7402ead99ca1d03abbb7
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7735462
Commit-Queue: Adam Rice <ricea@chromium.org>
Reviewed-by: Nidhi Jaju <nidhijaju@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1611366}
---

diff --git a/services/network/socket_data_pump.cc b/services/network/socket_data_pump.cc
index 35c3689..d1be429 100644
--- a/services/network/socket_data_pump.cc
+++ b/services/network/socket_data_pump.cc
@@ -4,6 +4,7 @@
 
 #include "services/network/socket_data_pump.h"
 
+#include <algorithm>
 #include <optional>
 #include <utility>
 
@@ -21,6 +22,26 @@
 
 namespace network {
 
+namespace {
+
+// This is similar to MojoToNetIOBuffer, but only keeps the pending_buffer
+// alive for the duration of the write, without assuming that the entire
+// buffer will be written.
+class MojoToNetPendingBufferOwningIOBuffer : public net::WrappedIOBuffer {
+ public:
+  explicit MojoToNetPendingBufferOwningIOBuffer(
+      scoped_refptr<MojoToNetPendingBuffer> pending_buffer)
+      : net::WrappedIOBuffer(*pending_buffer),
+        pending_buffer_(std::move(pending_buffer)) {}
+
+ private:
+  ~MojoToNetPendingBufferOwningIOBuffer() override = default;
+
+  scoped_refptr<MojoToNetPendingBuffer> pending_buffer_;
+};
+
+}  // namespace
+
 SocketDataPump::SocketDataPump(
     net::StreamSocket* socket,
     Delegate* delegate,
@@ -198,7 +219,9 @@
     ShutdownSend();
     return;
   }
-  auto buf = base::MakeRefCounted<net::WrappedIOBuffer>(*pending_send_buffer_);
+
+  auto buf = base::MakeRefCounted<MojoToNetPendingBufferOwningIOBuffer>(
+      pending_send_buffer_);
 
   // Use WeakPtr here because |this| doesn't outlive |socket_|.
   int write_result =
diff --git a/services/network/socket_data_pump_unittest.cc b/services/network/socket_data_pump_unittest.cc
index c9193cf..daa52437 100644
--- a/services/network/socket_data_pump_unittest.cc
+++ b/services/network/socket_data_pump_unittest.cc
@@ -2,6 +2,8 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+#include "services/network/socket_data_pump.h"
+
 #include <stdint.h>
 
 #include <utility>
@@ -10,12 +12,21 @@
 #include "base/containers/span.h"
 #include "base/functional/callback_helpers.h"
 #include "base/location.h"
+#include "base/logging.h"
 #include "base/run_loop.h"
+#include "base/strings/string_number_conversions.h"
+#include "base/strings/stringprintf.h"
 #include "base/test/task_environment.h"
 #include "mojo/public/cpp/system/simple_watcher.h"
+#include "net/base/io_buffer.h"
+#include "net/base/ip_address.h"
+#include "net/base/ip_endpoint.h"
 #include "net/base/net_errors.h"
 #include "net/base/test_completion_callback.h"
+#include "net/log/net_log_source.h"
 #include "net/socket/socket_test_util.h"
+#include "net/socket/tcp_client_socket.h"
+#include "net/socket/tcp_server_socket.h"
 #include "net/traffic_annotation/network_traffic_annotation_test_helper.h"
 #include "net/url_request/url_request_test_util.h"
 #include "services/network/mojo_socket_test_util.h"
@@ -342,4 +353,160 @@
   delegate()->WaitForShutdown();
 }
 
+namespace {
+
+class PumpDestroyingDelegate : public SocketDataPump::Delegate {
+ public:
+  PumpDestroyingDelegate() = default;
+  ~PumpDestroyingDelegate() = default;
+
+  PumpDestroyingDelegate(const PumpDestroyingDelegate&) = delete;
+  PumpDestroyingDelegate& operator=(const PumpDestroyingDelegate&) = delete;
+
+  void set_pump(std::unique_ptr<SocketDataPump> pump) {
+    pump_ = std::move(pump);
+  }
+
+  void set_run_on_shutdown(base::OnceClosure closure) {
+    run_on_shutdown_ = std::move(closure);
+  }
+
+  // SocketDataPump::Delegate implementation:
+  void OnNetworkReadError(int net_error) override {}
+  void OnNetworkWriteError(int net_error) override {}
+  void OnShutdown() override {
+    pump_ = nullptr;
+    if (run_on_shutdown_) {
+      std::move(run_on_shutdown_).Run();
+    }
+  }
+
+ private:
+  std::unique_ptr<SocketDataPump> pump_;
+  base::OnceClosure run_on_shutdown_;
+};
+
+// A dummy socket that behaves as if writes are blocked until the TakeWrite()
+// method is explicitly called.
+class BlockedStreamSocket : public net::StreamSocket {
+ public:
+  BlockedStreamSocket() = default;
+  ~BlockedStreamSocket() override = default;
+
+  void set_run_on_write(base::OnceClosure closure) {
+    run_on_write_ = std::move(closure);
+  }
+
+  std::vector<uint8_t> TakeWrite() {
+    size_t buf_len = pending_write_buf_len_;
+    std::vector data(std::from_range, pending_write_buf_->first(buf_len));
+    pending_write_buf_ = nullptr;
+    pending_write_buf_len_ = 0;
+    std::move(pending_write_callback_).Run(base::checked_cast<int>(buf_len));
+    return data;
+  }
+
+  // net::StreamSocket implementation:
+  int Read(net::IOBuffer* buf,
+           int buf_len,
+           net::CompletionOnceCallback callback) override {
+    return net::ERR_IO_PENDING;
+  }
+  int ReadIfReady(net::IOBuffer* buf,
+                  int buf_len,
+                  net::CompletionOnceCallback callback) override {
+    return net::ERR_IO_PENDING;
+  }
+  int CancelReadIfReady() override { return net::OK; }
+  int Write(
+      net::IOBuffer* buf,
+      int buf_len,
+      net::CompletionOnceCallback callback,
+      const net::NetworkTrafficAnnotationTag& traffic_annotation) override {
+    pending_write_buf_ = buf;
+    pending_write_buf_len_ = base::checked_cast<size_t>(buf_len);
+    pending_write_callback_ = std::move(callback);
+    if (run_on_write_) {
+      std::move(run_on_write_).Run();
+    }
+    return net::ERR_IO_PENDING;
+  }
+  int SetReceiveBufferSize(int32_t size) override { return net::OK; }
+  int SetSendBufferSize(int32_t size) override { return net::OK; }
+  int Connect(net::CompletionOnceCallback callback) override { return net::OK; }
+  void Disconnect() override {}
+  bool IsConnected() const override { return true; }
+  bool IsConnectedAndIdle() const override { return false; }
+  int GetPeerAddress(net::IPEndPoint* address) const override {
+    return net::OK;
+  }
+  int GetLocalAddress(net::IPEndPoint* address) const override {
+    return net::OK;
+  }
+  const net::NetLogWithSource& NetLog() const override { return net_log_; }
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/services/network/socket_data_pump_unittest.cc b/services/network/socket_data_pump_unittest.cc
index c9193cf..daa52437 100644
--- a/services/network/socket_data_pump_unittest.cc
+++ b/services/network/socket_data_pump_unittest.cc
@@ -2,6 +2,8 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+#include "services/network/socket_data_pump.h"
+
 #include <stdint.h>
 
 #include <utility>
@@ -10,12 +12,21 @@
 #include "base/containers/span.h"
 #include "base/functional/callback_helpers.h"
 #include "base/location.h"
+#include "base/logging.h"
 #include "base/run_loop.h"
+#include "base/strings/string_number_conversions.h"
+#include "base/strings/stringprintf.h"
 #include "base/test/task_environment.h"
 #include "mojo/public/cpp/system/simple_watcher.h"
+#include "net/base/io_buffer.h"
+#include "net/base/ip_address.h"
+#include "net/base/ip_endpoint.h"
 #include "net/base/net_errors.h"
 #include "net/base/test_completion_callback.h"
+#include "net/log/net_log_source.h"
 #include "net/socket/socket_test_util.h"
+#include "net/socket/tcp_client_socket.h"
+#include "net/socket/tcp_server_socket.h"
 #include "net/traffic_annotation/network_traffic_annotation_test_helper.h"
 #include "net/url_request/url_request_test_util.h"
 #include "services/network/mojo_socket_test_util.h"
@@ -342,4 +353,160 @@
   delegate()->WaitForShutdown();
 }
 
+namespace {
+
+class PumpDestroyingDelegate : public SocketDataPump::Delegate {
+ public:
+  PumpDestroyingDelegate() = default;
+  ~PumpDestroyingDelegate() = default;
+
+  PumpDestroyingDelegate(const PumpDestroyingDelegate&) = delete;
+  PumpDestroyingDelegate& operator=(const PumpDestroyingDelegate&) = delete;
+
+  void set_pump(std::unique_ptr<SocketDataPump> pump) {
+    pump_ = std::move(pump);
+  }
+
+  void set_run_on_shutdown(base::OnceClosure closure) {
+    run_on_shutdown_ = std::move(closure);
+  }
+
+  // SocketDataPump::Delegate implementation:
+  void OnNetworkReadError(int net_error) override {}
+  void OnNetworkWriteError(int net_error) override {}
+  void OnShutdown() override {
+    pump_ = nullptr;
+    if (run_on_shutdown_) {
+      std::move(run_on_shutdown_).Run();
+    }
+  }
+
+ private:
+  std::unique_ptr<SocketDataPump> pump_;
+  base::OnceClosure run_on_shutdown_;
+};
+
+// A dummy socket that behaves as if writes are blocked until the TakeWrite()
+// method is explicitly called.
+class BlockedStreamSocket : public net::StreamSocket {
+ public:
+  BlockedStreamSocket() = default;
+  ~BlockedStreamSocket() override = default;
+
+  void set_run_on_write(base::OnceClosure closure) {
+    run_on_write_ = std::move(closure);
+  }
+
+  std::vector<uint8_t> TakeWrite() {
+    size_t buf_len = pending_write_buf_len_;
+    std::vector data(std::from_range, pending_write_buf_->first(buf_len));
+    pending_write_buf_ = nullptr;
+    pending_write_buf_len_ = 0;
+    std::move(pending_write_callback_).Run(base::checked_cast<int>(buf_len));
+    return data;
+  }
+
+  // net::StreamSocket implementation:
+  int Read(net::IOBuffer* buf,
+           int buf_len,
+           net::CompletionOnceCallback callback) override {
+    return net::ERR_IO_PENDING;
+  }
+  int ReadIfReady(net::IOBuffer* buf,
+                  int buf_len,
+                  net::CompletionOnceCallback callback) override {
+    return net::ERR_IO_PENDING;
+  }
+  int CancelReadIfReady() override { return net::OK; }
+  int Write(
+      net::IOBuffer* buf,
+      int buf_len,
+      net::CompletionOnceCallback callback,
+      const net::NetworkTrafficAnnotationTag& traffic_annotation) override {
+    pending_write_buf_ = buf;
+    pending_write_buf_len_ = base::checked_cast<size_t>(buf_len);
+    pending_write_callback_ = std::move(callback);
+    if (run_on_write_) {
+      std::move(run_on_write_).Run();
+    }
+    return net::ERR_IO_PENDING;
+  }
+  int SetReceiveBufferSize(int32_t size) override { return net::OK; }
+  int SetSendBufferSize(int32_t size) override { return net::OK; }
+  int Connect(net::CompletionOnceCallback callback) override { return net::OK; }
+  void Disconnect() override {}
+  bool IsConnected() const override { return true; }
+  bool IsConnectedAndIdle() const override { return false; }
+  int GetPeerAddress(net::IPEndPoint* address) const override {
+    return net::OK;
+  }
+  int GetLocalAddress(net::IPEndPoint* address) const override {
+    return net::OK;
+  }
+  const net::NetLogWithSource& NetLog() const override { return net_log_; }
+  bool WasEverUsed() const override { return true; }
+  net::NextProto GetNegotiatedProtocol() const override {
+    return net::NextProto::kProtoUnknown;
+  }
+  bool GetSSLInfo(net::SSLInfo* ssl_info) override { return false; }
+  int64_t GetTotalReceivedBytes() const override { return 0; }
+  void ApplySocketTag(const net::SocketTag& tag) override {}
+
+ private:
+  scoped_refptr<net::IOBuffer> pending_write_buf_;
+  size_t pending_write_buf_len_ = 0;
+  net::CompletionOnceCallback pending_write_callback_;
+  base::OnceClosure run_on_write_;
+  net::NetLogWithSource net_log_;
+};
+
+}  // namespace
+
+TEST(SocketDataPumpTest, ShutdownWhileBlockedOnWrite) {
+  base::test::TaskEnvironment task_environment(
+      base::test::TaskEnvironment::MainThreadType::IO);
+
+  BlockedStreamSocket socket;
+  base::RunLoop wait_for_write;
+  socket.set_run_on_write(wait_for_write.QuitClosure());
+
+  mojo::ScopedDataPipeProducerHandle send_producer;
+  mojo::ScopedDataPipeConsumerHandle send_consumer;
+  ASSERT_EQ(MOJO_RESULT_OK,
+            mojo::CreateDataPipe(nullptr, send_producer, send_consumer));
+
+  mojo::ScopedDataPipeProducerHandle receive_producer;
+  mojo::ScopedDataPipeConsumerHandle receive_consumer;
+  ASSERT_EQ(MOJO_RESULT_OK,
+            mojo::CreateDataPipe(nullptr, receive_producer, receive_consumer));
+
+  PumpDestroyingDelegate delegate;
+  auto pump = std::make_unique<SocketDataPump>(
+      &socket, &delegate, std::move(receive_producer), std::move(send_consumer),
+      TRAFFIC_ANNOTATION_FOR_TESTS);
+  delegate.set_pump(std::move(pump));
+  base::RunLoop wait_for_shutdown;
+  delegate.set_run_on_shutdown(wait_for_shutdown.QuitClosure());
+
+  // Write data to the send pipe.
+  const std::string data = "secret";
+  size_t actually_written = 0;
+  ASSERT_EQ(MOJO_RESULT_OK, send_producer->WriteData(base::as_byte_span(data),
+                                                     MOJO_WRITE_DATA_FLAG_NONE,
+                                                     actually_written));
+  EXPECT_EQ(actually_written, data.size());
+
+  // Run until SocketDataPump reads from the pipe and calls socket.Write().
+  wait_for_write.Run();
+
+  // Trigger OnShutdown() by closing the receive consumer.
+  // This will cause PumpDestroyingDelegate to destroy the SocketDataPump.
+  receive_consumer.reset();
+  wait_for_shutdown.Run();
+
+  // Copy the data that was passed to Write().
+  std::vector<uint8_t> written_data = socket.TakeWrite();
+  EXPECT_EQ(data, base::as_string_view(written_data));
+}
+
 }  // namespace network
Loading diff…

Original Bug Report

reported by vm...@google.com

Potential Use-After-Unmap info leak in SocketDataPump due to flawed idle check

Project Fortify, an experimental security project, has identified the following potential security issue.

Overview: SocketDataPump incorrectly determines if its send path is idle because it checks send_stream_.is_valid() which becomes false during an active pending write. This logic flaw allows the pump to be prematurely destroyed, unmapping the Mojo shared memory while the underlying socket retains a dangling pointer to it. When the socket eventually becomes writable, it may send remapped sensitive cross-origin data to an attacker-controlled server.

Affected files:

  • services/network/socket_data_pump.cc
  • services/network/tcp_connected_socket.cc
  • services/network/proxy_resolving_socket_mojo.cc

Estimated timestamp from git blame: 2024-09-03

Vulnerability Description

A potential Use-After-Unmap (UAF/UAU) vulnerability exists in services/network/socket_data_pump.cc due to a logic flaw in how SocketDataPump determines if it is safe to shut down.

When evaluating if both the send and receive paths are finished, SocketDataPump::MaybeNotifyDelegate() checks if send_stream_.is_valid() is true:

void SocketDataPump::MaybeNotifyDelegate() {
  if (!delegate_ || send_stream_.is_valid() || !receive_is_shutdown_) {
    return;
  }
  delegate_->OnShutdown();
}

However, this check is insufficient. When SocketDataPump::SendMore() initiates a write, it calls MojoToNetPendingBuffer::BeginRead(), which std::move()s the send_stream_ handle into a pending_send_buffer_. As a result, send_stream_.is_valid() evaluates to false for the entire duration of the socket write operation.

If the socket returns net::ERR_IO_PENDING (e.g., because the OS send buffer is full), the write remains pending asynchronously. If the receive stream is closed during this time, MaybeNotifyDelegate() will incorrectly conclude that the pump is completely idle and call delegate_->OnShutdown().

Attack Scenario

Note: These are potential steps based on code analysis; our tooling agent does not currently have the ability to run a live Proof of Concept.

  1. Connection Setup: An attacker in a compromised renderer with socket access (e.g., a Chrome Extension or an Isolated Web App via Direct Sockets) opens a TCP connection to a malicious server.
  2. Induce a Pending Write: The malicious server refuses to read from the TCP socket, causing the OS send buffer to fill up. The renderer writes a large payload to the socket’s Mojo data pipe. The Network Service attempts to write this data, but socket_->Write() returns net::ERR_IO_PENDING.
  3. Trigger Shutdown: The attacker intentionally closes the receive-side Mojo data pipe from the renderer.
  4. Premature Destruction: SocketDataPump::OnReceiveStreamClosed() is triggered, setting receive_is_shutdown_ = true and calling MaybeNotifyDelegate(). Since send_stream_ is currently invalid (held by pending_send_buffer_), the pump incorrectly notifies its delegate (TCPConnectedSocket) to shut down.
  5. Memory Unmapping: TCPConnectedSocket::OnShutdown() destroys the SocketDataPump, which destroys the pending_send_buffer_ and fully closes the Mojo data pipe. The OS unmaps the shared memory backing the pipe.
  6. Dangling Pointer: Crucially, TCPConnectedSocket keeps the underlying net::TransportClientSocket alive (to support potential TLS upgrades). The socket (e.g., SocketPosix) still holds a net::WrappedIOBuffer pointing to the now-unmapped virtual address.
  7. Memory Spray & Exfiltration: The attacker initiates hundreds of cross-origin fetch() requests. The Network Service allocates new Mojo data pipes for the sensitive HTTP responses, and the OS remaps the recently freed virtual address space with this new data.
  8. Data Leak: The malicious server finally reads from the TCP socket, clearing the OS send buffer. The socket becomes writable, and the asynchronous write resumes (e.g., SocketPosix::WriteCompleted calls DoWrite()). The kernel reads from the dangling pointer, silently transmitting the remapped sensitive cross-origin data to the attacker’s server.

Mitigation Bypass

Chromium’s MiraclePtr (BackupRefPtr) does not protect against this vulnerability because the dangling pointer inside net::WrappedIOBuffer points to mmap’d shared memory (the Mojo data pipe), not memory allocated via PartitionAlloc.

Suggested Fix

Modify SocketDataPump::MaybeNotifyDelegate() to also check if there is an active pending write. For example:

void SocketDataPump::MaybeNotifyDelegate() {
  if (!delegate_ || send_stream_.is_valid() || pending_send_buffer_ || !receive_is_shutdown_) {
    return;
  }
  delegate_->OnShutdown();
}

Alternatively, ensure that TCPConnectedSocket::OnShutdown() or SocketDataPump::~SocketDataPump() explicitly cancels any pending I/O operations on the underlying socket_ before allowing the object to be destroyed or the socket to be repurposed.

Evaluated with Chrome root at commit: a9cbf6e8b275fe4147435aa905f3b7f5a656f5f0


Results from 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. And please feel free to reach out to me directly if you have concerns or feedback on the project.

View on issue tracker