Overview

Critical
Severity
8.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Cast
DescriptionUse after free in Cast
ComponentCast
Bug ClassUAF
Action tierwatch — corruption primitive but gated (uncommon) — not default-reachable
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.
Tracker553770012
Fix commit46be9fe1d78e (chromium/src) +58/-24
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:A/AC:H/PR:N/UI:N/S:C/C:H/I:H/A:H
CISA KEVNot listed
CreditedHafiizh
Reported2026-08-28 (per vendor advisory)
Fix landed2026-08-31 (public git)
Disclosed2026-09-08
Patch gap8 days — fix public in source before the advisory

Background

`NetUdpSocket`
Chromium’s Open Screen platform adapter in components/openscreen_platform/net_udp_socket.cc that wraps a net::UDPSocket behind the openscreen::UdpSocket interface used by Cast discovery and streaming.
`Client` callbacks
the openscreen::UdpSocket::Client methods (OnBound, OnRead, OnError, OnSendError) that NetUdpSocket invokes synchronously through its client_ pointer, and which are allowed to delete the socket from inside the callback.
`base::WeakPtr` / `base::WeakPtrFactory`
Chromium smart pointers whose WeakPtr evaluates to null once the owning object is destroyed, letting a method detect that its own this was 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 this or 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.

Key insight
The single mistake is treating synchronous 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

  1. Obtain a socket event that fires a `Client` callback Drive the Cast/Open Screen path so NetUdpSocket reaches a notification point: Bind completing (OnBound), an inbound datagram or read failure (OnRead), a bind/open error (OnError), or a send completion/error (OnSendError).
  2. Destroy the socket inside the callback Arrange for the openscreen::UdpSocket::Client to synchronously tear down the NetUdpSocket while handling that callback, which frees this mid-method.
  3. Return into freed-object code Control returns to the vulnerable site, which uses the freed this: Bind calls DoRead() after OnBound; DoRead’s while (HandleRecvFromResult(...)) re-enters after OnRead returned true; or SendTo writes send_pending_ = true after a synchronous completion routed through OnSendToCompleted/OnSendError.
  4. Use-after-free The dereference of the freed NetUdpSocket (or the base::Unretained(this) completion callback firing later against freed memory) is the use-after-free reported by the advisory.
  5. 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

An attacker who can cause a 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

FunctionChangeNotes
RunClientCallback
components/openscreen_platform/net_udp_socket.cc
modified
if
components/openscreen_platform/net_udp_socket.cc
modified

Files Changed

  • components/openscreen_platform/net_udp_socket.cc
  • components/openscreen_platform/net_udp_socket.h

Audit Directions

  • Reentrant client/delegate callbacks
    Audit any adapter that holds a raw client_/delegate pointer and invokes it synchronously, then continues to touch this or members; require a WeakPtr liveness check after each such call.
  • `base::Unretained(this)` in async socket completions
    Flag net::UDPSocket (and similar) RecvFrom/SendTo/Read/Write calls that bind completions with base::Unretained(this) where the completion may run after a callback-driven teardown; prefer WeakPtr-bound callbacks.
  • Post-callback state writes and loops
    Look for while (...) read/pump loops and post-notification assignments (e.g. a pending/in_flight flag) that execute after a synchronous callback, since these are the sinks where a reentrant free becomes a use-after-free.
From 46be9fe1d78e9fa53a3bb4d1a401207e1a7ebb07 Mon Sep 17 00:00:00 2001
From: Thomas Guilbert <[email protected]>
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 <[email protected]>
Commit-Queue: Thomas Guilbert <[email protected]>
Reviewed-by: Ted (Chromium) Meyer <[email protected]>
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
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.