CVE-2026-87628
Overview
Background
- `NetUdpSocket`
- Chromium’s Open Screen platform adapter in
components/openscreen_platform/net_udp_socket.ccthat wraps anet::UDPSocketbehind theopenscreen::UdpSocketinterface used by Cast discovery and streaming. - `Client` callbacks
- the
openscreen::UdpSocket::Clientmethods (OnBound,OnRead,OnError,OnSendError) thatNetUdpSocketinvokes synchronously through itsclient_pointer, and which are allowed to delete the socket from inside the callback. - `base::WeakPtr` / `base::WeakPtrFactory`
- Chromium smart pointers whose
WeakPtrevaluates to null once the owning object is destroyed, letting a method detect that its ownthiswas freed during a reentrant call. - Reentrant destruction
- the pattern where a synchronous callback destroys the object whose method is still on the stack, making every subsequent access to
thisor a member a use-after-free.
Root Cause Analysis
NetUdpSocket dispatched socket events by calling client_->OnBound, client_->OnRead, client_->OnError, and client_->OnSendError directly, and then continued to use this after the call returned. Because those Client callbacks may synchronously destroy the NetUdpSocket, the code violated the invariant that this remains valid for the rest of the method: after client_->OnBound(this) the Bind path unconditionally called DoRead(); the read loop returned true after client_->OnRead(this, packet) so DoRead()’s while (HandleRecvFromResult(...)) would iterate again on a freed object; and the asynchronous RecvFrom/SendTo completions were bound with base::Unretained(this), with send_pending_ = true written after a SendTo that could complete synchronously and free this.
The fix adds a base::WeakPtrFactory and funnels every client notification through RunClientCallback, which captures a WeakPtr before invoking the callback and returns whether this is still alive; callers then bail out (return, stop the loop, or skip DoRead()) when the object was destroyed, and the completion callbacks are rebound to weak_ptr_factory_.GetWeakPtr(). This works because each reentrant callback is now followed by a liveness check that runs before any further use of this.
client_ callbacks as if they could not destroy the socket, so this (and members like send_pending_) were used after a reentrant delete; the fix gates every callback through RunClientCallback, whose WeakPtr return value forces the caller to stop touching this once it has been freed.Attack Path
- Obtain a socket event that fires a `Client` callback
Drive the Cast/Open Screen path so
NetUdpSocketreaches a notification point:Bindcompleting (OnBound), an inbound datagram or read failure (OnRead), a bind/open error (OnError), or a send completion/error (OnSendError). - Destroy the socket inside the callback
Arrange for the
openscreen::UdpSocket::Clientto synchronously tear down theNetUdpSocketwhile handling that callback, which freesthismid-method. - Return into freed-object code
Control returns to the vulnerable site, which uses the freed
this:BindcallsDoRead()afterOnBound;DoRead’swhile (HandleRecvFromResult(...))re-enters afterOnReadreturnedtrue; orSendTowritessend_pending_ = trueafter a synchronous completion routed throughOnSendToCompleted/OnSendError. - Use-after-free
The dereference of the freed
NetUdpSocket(or thebase::Unretained(this)completion callback firing later against freed memory) is the use-after-free reported by the advisory. - Exploitation primitive
Any memory-corruption escalation beyond the use-after-free (controlled reallocation, read/write primitive, code execution) is
(inferred); the patch demonstrates only the reentrant free-then-use.
Impact Assessment
NetUdpSocket client callback to synchronously destroy the socket gains a use-after-free of that object in the process that hosts Cast/Open Screen networking (the browser process, (inferred)), with the precondition of reaching the Cast discovery/streaming socket path and controlling the client teardown timing. This yields memory corruption; any further escalation to information disclosure or code execution is (inferred) and not shown by the patch. The vendor advisory rates this critical severity and does not report in-the-wild exploitation; NVD scores it CVSS 8.3. A severity rating are not stated in the provided advisory, so none is asserted here.Changed Functions
| Function | Change | Notes |
|---|---|---|
RunClientCallbackcomponents/openscreen_platform/net_udp_socket.cc |
modified | |
ifcomponents/openscreen_platform/net_udp_socket.cc |
modified |
Files Changed
components/openscreen_platform/net_udp_socket.cccomponents/openscreen_platform/net_udp_socket.h
Audit Directions
- Reentrant client/delegate callbacksAudit any adapter that holds a raw
client_/delegate pointer and invokes it synchronously, then continues to touchthisor members; require aWeakPtrliveness check after each such call. - `base::Unretained(this)` in async socket completionsFlag
net::UDPSocket(and similar)RecvFrom/SendTo/Read/Writecalls that bind completions withbase::Unretained(this)where the completion may run after a callback-driven teardown; preferWeakPtr-bound callbacks. - Post-callback state writes and loopsLook for
while (...)read/pump loops and post-notification assignments (e.g. apending/in_flightflag) that execute after a synchronous callback, since these are the sinks where a reentrant free becomes a use-after-free.
Patch
From 46be9fe1d78e9fa53a3bb4d1a401207e1a7ebb07 Mon Sep 17 00:00:00 2001
From: Thomas Guilbert <tguilbert@chromium.org>
Date: Mon, 31 Aug 2026 16:14:10 -0700
Subject: [PATCH] Fix UAF in openscreen_platform::NetUdpSocket
This CL adds weak pointer checks and routes client notifications through
a callback gate in NetUdpSocket to prevent a Use-After-Free if the
client's callbacks (e.g. OnRead, OnBound) synchronously destroy the
NetUdpSocket instance.
TAG=agy
CONV=3f403199-0d10-4bde-b609-19bcd1a13fd8
Bug: 553770012
Change-Id: I537bb98c9c0c33139a4c02a655e69d73f67bb171
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8309424
Auto-Submit: Jordan Bayles <jophba@chromium.org>
Commit-Queue: Thomas Guilbert <tguilbert@chromium.org>
Reviewed-by: Ted (Chromium) Meyer <tmathmeyer@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1689377}
---
diff --git a/components/openscreen_platform/net_udp_socket.cc b/components/openscreen_platform/net_udp_socket.cc
index d2d968e..9334890 100644
--- a/components/openscreen_platform/net_udp_socket.cc
+++ b/components/openscreen_platform/net_udp_socket.cc
@@ -47,17 +47,21 @@
void NetUdpSocket::SendErrorToClient(openscreen::Error::Code openscreen_error,
int net_error) {
DVLOG(1) << __func__;
- client_->OnError(
- this, openscreen::Error(openscreen_error, net::ErrorToString(net_error)));
+ RunClientCallback([this, openscreen_error, net_error](Client& client) {
+ client.OnError(this, openscreen::Error(openscreen_error,
+ net::ErrorToString(net_error)));
+ });
}
void NetUdpSocket::DoRead() {
DVLOG(3) << __func__;
- while (HandleRecvFromResult(udp_socket_.RecvFrom(
- read_buffer_.get(), openscreen::UdpPacket::kUdpMaxPacketSize,
- &from_address_,
- base::BindOnce(&NetUdpSocket::OnRecvFromCompleted,
- base::Unretained(this))))) {
+ base::WeakPtr<NetUdpSocket> weak_this = weak_ptr_factory_.GetWeakPtr();
+ while (weak_this &&
+ HandleRecvFromResult(udp_socket_.RecvFrom(
+ read_buffer_.get(), openscreen::UdpPacket::kUdpMaxPacketSize,
+ &from_address_,
+ base::BindOnce(&NetUdpSocket::OnRecvFromCompleted,
+ weak_ptr_factory_.GetWeakPtr())))) {
}
}
@@ -69,9 +73,11 @@
}
if (result < 0) {
- client_->OnRead(
- this, openscreen::Error(openscreen::Error::Code::kSocketReadFailure,
- net::ErrorToString(result)));
+ RunClientCallback([this, result](Client& client) {
+ client.OnRead(
+ this, openscreen::Error(openscreen::Error::Code::kSocketReadFailure,
+ net::ErrorToString(result)));
+ });
return false;
}
@@ -80,8 +86,9 @@
openscreen::UdpPacket packet(read_buffer_->data(),
UNSAFE_TODO(read_buffer_->data() + result));
packet.set_source(openscreen_platform::ToOpenScreenEndPoint(from_address_));
- client_->OnRead(this, std::move(packet));
- return true;
+ return RunClientCallback([this, &packet](Client& client) {
+ client.OnRead(this, std::move(packet));
+ });
}
void NetUdpSocket::OnRecvFromCompleted(int result) {
@@ -95,9 +102,11 @@
DVLOG(3) << __func__;
send_pending_ = false;
if (result < 0) {
- client_->OnSendError(
- this, openscreen::Error(openscreen::Error::Code::kSocketSendFailure,
- net::ErrorToString(result)));
+ RunClientCallback([this, result](Client& client) {
+ client.OnSendError(
+ this, openscreen::Error(openscreen::Error::Code::kSocketSendFailure,
+ net::ErrorToString(result)));
+ });
}
}
@@ -121,12 +130,9 @@
net::IPEndPoint endpoint =
openscreen_platform::ToNetEndPoint(local_endpoint_);
int result = udp_socket_.Open(endpoint.GetFamily());
- if (result != net::OK) {
- SendErrorToClient(openscreen::Error::Code::kSocketBindFailure, result);
- return;
+ if (result == net::OK) {
+ result = udp_socket_.Bind(endpoint);
}
-
- result = udp_socket_.Bind(endpoint);
net::IPEndPoint local_endpoint;
if (result == net::OK) {
result = udp_socket_.GetLocalAddress(&local_endpoint);
@@ -138,8 +144,9 @@
}
local_endpoint_ = openscreen_platform::ToOpenScreenEndPoint(local_endpoint);
- client_->OnBound(this);
- DoRead();
+ if (RunClientCallback([this](Client& client) { client.OnBound(this); })) {
+ DoRead();
+ }
}
void NetUdpSocket::SetMulticastOutboundInterface(
@@ -170,17 +177,25 @@
DVLOG(3) << __func__;
if (send_pending_) {
- client_->OnSendError(this,
+ RunClientCallback([this](Client& client) {
+ client.OnSendError(this,
openscreen::Error(openscreen::Error::Code::kAgain));
+ });
return;
}
auto buffer = base::MakeRefCounted<net::IOBufferWithSize>(data.size());
UNSAFE_TODO(memcpy(buffer->data(), data.data(), data.size()));
+ base::WeakPtr<NetUdpSocket> weak_this = weak_ptr_factory_.GetWeakPtr();
const int result = udp_socket_.SendTo(
buffer.get(), data.size(), openscreen_platform::ToNetEndPoint(dest),
- base::BindOnce(&NetUdpSocket::OnSendToCompleted, base::Unretained(this)));
+ base::BindOnce(&NetUdpSocket::OnSendToCompleted,
+ weak_ptr_factory_.GetWeakPtr()));
+ if (!weak_this) {
+ return;
+ }
+
send_pending_ = true;
if (result != net::ERR_IO_PENDING) {
diff --git a/components/openscreen_platform/net_udp_socket.h b/components/openscreen_platform/net_udp_socket.h
index 7af1f0ef..da3400ed 100644
--- a/components/openscreen_platform/net_udp_socket.h
+++ b/components/openscreen_platform/net_udp_socket.h
@@ -5,7 +5,10 @@
#ifndef COMPONENTS_OPENSCREEN_PLATFORM_NET_UDP_SOCKET_H_
#define COMPONENTS_OPENSCREEN_PLATFORM_NET_UDP_SOCKET_H_
+#include <utility>
+
#include "base/memory/raw_ptr.h"
+#include "base/memory/weak_ptr.h"
#include "net/base/io_buffer.h"
#include "net/base/ip_endpoint.h"
#include "net/socket/udp_socket.h"
@@ -26,10 +29,24 @@
NetUdpSocket& operator=(NetUdpSocket&&) = delete;
private:
+ // Dispatches a callback to `client_` and returns true if `*this` is still
+ // alive. Note: `client_` callbacks may synchronously destroy `this`.
+ template <typename Callback>
+ bool RunClientCallback(Callback&& callback) {
+ base::WeakPtr<NetUdpSocket> weak_this = weak_ptr_factory_.GetWeakPtr();
+ std::forward<Callback>(callback)(*client_);
+ return static_cast<bool>(weak_this);
+ }
+
void SendErrorToClient(openscreen::Error::Code openscreen_error,
int net_error);
void DoRead();
+
+ // Dispatches read data or error to `client_`. Returns true if more data
+ // should be read synchronously, or false if pending, on error, or if `this`
+ // was destroyed. Note: `client_` callbacks may synchronously destroy `this`.
bool HandleRecvFromResult(int result);
+
void OnRecvFromCompleted(int result);
void OnSendToCompleted(int result);
@@ -55,6 +72,8 @@
scoped_refptr<net::IOBuffer> read_buffer_;
net::IPEndPoint from_address_;
bool send_pending_ = false;
+
+ base::WeakPtrFactory<NetUdpSocket> weak_ptr_factory_{this};
};
} // namespace openscreen_platform