Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Chromecast
DescriptionUse after free in Chromecast
ComponentChromecast
Bug ClassUAF
Tracker521285077
Fix commit2991d533dd22 (chromium/src) +141/-42
CISA KEVNot listed
CreditedGoogle
Disclosed2026-08-25

Changed Functions

FunctionChangeNotes
TestGestureSource
chromecast/browser/cast_web_contents_browsertest.cc
modified
if
chromecast/browser/cast_web_contents_browsertest.cc
modified
if
chromecast/external_mojo/public/cpp/external_mojo_broker.cc
modified

Files Changed

  • chromecast/browser/BUILD.gn
  • chromecast/browser/cast_web_contents_browsertest.cc
  • chromecast/external_mojo/public/cpp/external_mojo_broker.cc
  • chromecast/renderer/cast_demo_bindings.cc
From 2991d533dd224fa6297a16859a775777d88bd129 Mon Sep 17 00:00:00 2001
From: Simeon Anfinrud <[email protected]>
Date: Mon, 27 Jul 2026 12:46:53 -0700
Subject: [PATCH] [chromecast] Don't access CastBinding members after JS callbacks

CastWindowManagerBindings, SettingsUiBindings and CastDemoBindings
invoke page-supplied JS callbacks and then write back the
v8::UniquePersistent handler member. The callback can detach its own
frame, which deletes the CastBinding (RenderFrameObserver self-deletes
in OnDestruct()), so the post-Call() member write runs on a destroyed
object.

v8::Local<T>::New() takes the persistent by const reference, so the
std::move() / restore was a no-op anyway. Drop both so the handlers
don't touch |this| after running script. Also swap the pending
platform-info payload onto the stack before delivering it in
SetPlatformInfoHandler().

This also means a page that re-registers a handler from inside the
callback now keeps the new handler instead of having it clobbered by the
restore.

Also add v8::MicrotasksScope to InvokeV8Callback overloads and VolumeChanged
callbacks to prevent V8 debug check failures when called outside the Blink
task runner.

Add a cast_shell_browsertests case that dispatches a tap gesture to a
subframe whose handler removes its own frameElement and checks that the
renderer is still responsive afterwards.

Bug: 521285077
Test: cast_shell_browsertests
Change-Id: Iaa8a0d0fbd0a21464f73b9897882310f16411138
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8009010
Auto-Submit: Simeon Anfinrud <[email protected]>
Commit-Queue: Simeon Anfinrud <[email protected]>
Reviewed-by: Shawn Quereshi <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1668893}
---

diff --git a/chromecast/browser/BUILD.gn b/chromecast/browser/BUILD.gn
index 0f1a647..3d2e0ea 100644
--- a/chromecast/browser/BUILD.gn
+++ b/chromecast/browser/BUILD.gn
@@ -531,6 +531,7 @@
     "//chromecast/base",
     "//chromecast/base:chromecast_switches",
     "//chromecast/base/metrics",
+    "//chromecast/common:feature_constants",
     "//chromecast/graphics",
     "//chromecast/mojo",
     "//components/keyed_service/content",
diff --git a/chromecast/browser/cast_web_contents_browsertest.cc b/chromecast/browser/cast_web_contents_browsertest.cc
index 7350e44..fadc2fe 100644
--- a/chromecast/browser/cast_web_contents_browsertest.cc
+++ b/chromecast/browser/cast_web_contents_browsertest.cc
@@ -25,6 +25,8 @@
 #include "chromecast/browser/mojom/cast_web_service.mojom.h"
 #include "chromecast/browser/test/cast_browser_test.h"
 #include "chromecast/browser/test_interfaces.test-mojom.h"
+#include "chromecast/common/feature_constants.h"
+#include "chromecast/common/mojom/gesture.mojom.h"
 #include "chromecast/mojo/interface_bundle.h"
 #include "content/public/browser/browser_thread.h"
 #include "content/public/browser/web_contents.h"
@@ -1167,4 +1169,104 @@
   EXPECT_EQ(2u, provider.num_doublers());
 }
 
+// Browser-side mojom::GestureSource which captures the renderer's
+// GestureHandler so the test can dispatch gesture events directly.
+class TestGestureSource : public mojom::GestureSource {
+ public:
+  TestGestureSource() = default;
+  ~TestGestureSource() override = default;
+
+  void Bind(mojo::PendingReceiver<mojom::GestureSource> receiver) {
+    receivers_.Add(this, std::move(receiver));
+  }
+
+  void WaitForSubscribe() {
+    if (handler_) {
+      return;
+    }
+    base::RunLoop run_loop;
+    subscribed_closure_ = run_loop.QuitClosure();
+    run_loop.Run();
+  }
+
+  mojo::Remote<mojom::GestureHandler>& handler() { return handler_; }
+
+  // mojom::GestureSource:
+  void Subscribe(mojo::PendingRemote<mojom::GestureHandler> handler) override {
+    handler_.reset();
+    handler_.Bind(std::move(handler));
+    if (subscribed_closure_) {
+      std::move(subscribed_closure_).Run();
+    }
+  }
+  void SetCanGoBack(bool can_go_back) override {}
+  void SetCanTopDrag(bool can_top_drag) override {}
+  void SetCanRightDrag(bool can_right_drag) override {}
+
+ private:
+  mojo::ReceiverSet<mojom::GestureSource> receivers_;
+  mojo::Remote<mojom::GestureHandler> handler_;
+  base::OnceClosure subscribed_closure_;
+};
+
+IN_PROC_BROWSER_TEST_F(CastWebContentsBrowserTest,
+                       WindowManagerGestureCallbackDetachesFrame) {
+  // ===========================================================================
+  // Test: A page-supplied gesture callback may detach its own frame while it is
+  // running. The renderer must remain in a consistent state once the callback
+  // returns.
+  // ===========================================================================
+  TestGestureSource gesture_source;
+  cast_web_contents_->local_interfaces()->AddBinder(base::BindRepeating(
+      &TestGestureSource::Bind, base::Unretained(&gesture_source)));
+
+  base::DictValue features;
+  features.Set(feature::kEnableSystemGestures, base::DictValue());
+  cast_web_contents_->AddRendererFeatures(std::move(features));
+
+  run_loop_ = std::make_unique<base::RunLoop>();
+  {
+    InSequence seq;
+    EXPECT_CALL(mock_cast_wc_observer_, PageStateChanged(PageState::LOADING));
+    EXPECT_CALL(mock_cast_wc_observer_, PageStateChanged(PageState::LOADED))
+        .WillOnce(InvokeWithoutArgs([&]() { QuitRunLoop(); }));
+  }
+  cast_web_contents_->LoadUrl(GURL(url::kAboutBlankURL));
+  run_loop_->Run();
+
+  ASSERT_TRUE(ExecJs(web_contents_.get(),
+                     "var ifr = document.createElement('iframe');"
+                     "document.body.appendChild(ifr);"));
+  content::RenderFrameHost* child =
+      content::ChildFrameAt(web_contents_.get(), 0);
+  ASSERT_TRUE(child);
+
+  // Wait for the cast.__platform__.windowManager bindings to be installed in
+  // the subframe, register a tap callback that removes the frame and bind the
+  // GestureHandler to the browser-side source.
+  ASSERT_TRUE(ExecJs(child,
+                     "(async () => {"
+                     "  while (!self.cast || !cast.__platform__ ||"
+                     "         !cast.__platform__.windowManager) {"
+                     "    await new Promise(r => setTimeout(r, 0));"
+                     "  }"
+                     "  cast.__platform__.windowManager.onTapGesture("
+                     "      () => { frameElement.remove(); });"
+                     "  cast.__platform__.windowManager.canGoBack(true);"
+                     "})();"));
+  gesture_source.WaitForSubscribe();
+  ASSERT_TRUE(gesture_source.handler().is_bound());
+
+  // Dispatch the gesture; the JS callback synchronously detaches the frame.
+  base::RunLoop disconnect_loop;
+  gesture_source.handler().set_disconnect_handler(
+      disconnect_loop.QuitClosure());
+  gesture_source.handler()->OnTapGesture();
+  disconnect_loop.Run();
+
+  // The main frame's renderer must still be responsive.
+  EXPECT_EQ(true, content::EvalJs(web_contents_.get(),
+                                  "document.querySelector('iframe') === null"));
+}
+
 }  // namespace chromecast
diff --git a/chromecast/external_mojo/public/cpp/external_mojo_broker.cc b/chromecast/external_mojo/public/cpp/external_mojo_broker.cc
index 58f9f96e..1acedce1 100644
--- a/chromecast/external_mojo/public/cpp/external_mojo_broker.cc
+++ b/chromecast/external_mojo/public/cpp/external_mojo_broker.cc
@@ -17,6 +17,8 @@
 
 #include <optional>
 
+#include "base/files/file_path.h"
+#include "base/files/file_util.h"
 #include "base/functional/bind.h"
 #include "base/functional/callback_helpers.h"
 #include "base/location.h"
@@ -436,6 +438,10 @@
 
   LOG(INFO) << "Initializing external mojo broker at: " << broker_path;
 
+  if (!use_abstract_namespace) {
+    base::DeleteFile(base::FilePath(broker_path));
+  }
+
   mojo::NamedPlatformChannel::Options channel_options;
   channel_options.server_name = broker_path;
   channel_options.use_abstract_namespace = use_abstract_namespace;
diff --git a/chromecast/renderer/cast_demo_bindings.cc b/chromecast/renderer/cast_demo_bindings.cc
index 3e4d999..f5e13c505 100644
--- a/chromecast/renderer/cast_demo_bindings.cc
+++ b/chromecast/renderer/cast_demo_bindings.cc
@@ -332,20 +332,18 @@
   v8::Isolate* isolate = web_frame->GetAgentGroupScheduler()->Isolate();
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/chromecast/browser/cast_web_contents_browsertest.cc b/chromecast/browser/cast_web_contents_browsertest.cc
index 7350e44..fadc2fe 100644
--- a/chromecast/browser/cast_web_contents_browsertest.cc
+++ b/chromecast/browser/cast_web_contents_browsertest.cc
@@ -25,6 +25,8 @@
 #include "chromecast/browser/mojom/cast_web_service.mojom.h"
 #include "chromecast/browser/test/cast_browser_test.h"
 #include "chromecast/browser/test_interfaces.test-mojom.h"
+#include "chromecast/common/feature_constants.h"
+#include "chromecast/common/mojom/gesture.mojom.h"
 #include "chromecast/mojo/interface_bundle.h"
 #include "content/public/browser/browser_thread.h"
 #include "content/public/browser/web_contents.h"
@@ -1167,4 +1169,104 @@
   EXPECT_EQ(2u, provider.num_doublers());
 }
 
+// Browser-side mojom::GestureSource which captures the renderer's
+// GestureHandler so the test can dispatch gesture events directly.
+class TestGestureSource : public mojom::GestureSource {
+ public:
+  TestGestureSource() = default;
+  ~TestGestureSource() override = default;
+
+  void Bind(mojo::PendingReceiver<mojom::GestureSource> receiver) {
+    receivers_.Add(this, std::move(receiver));
+  }
+
+  void WaitForSubscribe() {
+    if (handler_) {
+      return;
+    }
+    base::RunLoop run_loop;
+    subscribed_closure_ = run_loop.QuitClosure();
+    run_loop.Run();
+  }
+
+  mojo::Remote<mojom::GestureHandler>& handler() { return handler_; }
+
+  // mojom::GestureSource:
+  void Subscribe(mojo::PendingRemote<mojom::GestureHandler> handler) override {
+    handler_.reset();
+    handler_.Bind(std::move(handler));
+    if (subscribed_closure_) {
+      std::move(subscribed_closure_).Run();
+    }
+  }
+  void SetCanGoBack(bool can_go_back) override {}
+  void SetCanTopDrag(bool can_top_drag) override {}
+  void SetCanRightDrag(bool can_right_drag) override {}
+
+ private:
+  mojo::ReceiverSet<mojom::GestureSource> receivers_;
+  mojo::Remote<mojom::GestureHandler> handler_;
+  base::OnceClosure subscribed_closure_;
+};
+
+IN_PROC_BROWSER_TEST_F(CastWebContentsBrowserTest,
+                       WindowManagerGestureCallbackDetachesFrame) {
+  // ===========================================================================
+  // Test: A page-supplied gesture callback may detach its own frame while it is
+  // running. The renderer must remain in a consistent state once the callback
+  // returns.
+  // ===========================================================================
+  TestGestureSource gesture_source;
+  cast_web_contents_->local_interfaces()->AddBinder(base::BindRepeating(
+      &TestGestureSource::Bind, base::Unretained(&gesture_source)));
+
+  base::DictValue features;
+  features.Set(feature::kEnableSystemGestures, base::DictValue());
+  cast_web_contents_->AddRendererFeatures(std::move(features));
+
+  run_loop_ = std::make_unique<base::RunLoop>();
+  {
+    InSequence seq;
+    EXPECT_CALL(mock_cast_wc_observer_, PageStateChanged(PageState::LOADING));
+    EXPECT_CALL(mock_cast_wc_observer_, PageStateChanged(PageState::LOADED))
+        .WillOnce(InvokeWithoutArgs([&]() { QuitRunLoop(); }));
+  }
+  cast_web_contents_->LoadUrl(GURL(url::kAboutBlankURL));
+  run_loop_->Run();
+
+  ASSERT_TRUE(ExecJs(web_contents_.get(),
+                     "var ifr = document.createElement('iframe');"
+                     "document.body.appendChild(ifr);"));
+  content::RenderFrameHost* child =
+      content::ChildFrameAt(web_contents_.get(), 0);
+  ASSERT_TRUE(child);
+
+  // Wait for the cast.__platform__.windowManager bindings to be installed in
+  // the subframe, register a tap callback that removes the frame and bind the
+  // GestureHandler to the browser-side source.
+  ASSERT_TRUE(ExecJs(child,
+                     "(async () => {"
+                     "  while (!self.cast || !cast.__platform__ ||"
+                     "         !cast.__platform__.windowManager) {"
+                     "    await new Promise(r => setTimeout(r, 0));"
+                     "  }"
+                     "  cast.__platform__.windowManager.onTapGesture("
+                     "      () => { frameElement.remove(); });"
+                     "  cast.__platform__.windowManager.canGoBack(true);"
+                     "})();"));
+  gesture_source.WaitForSubscribe();
+  ASSERT_TRUE(gesture_source.handler().is_bound());
+
+  // Dispatch the gesture; the JS callback synchronously detaches the frame.
+  base::RunLoop disconnect_loop;
+  gesture_source.handler().set_disconnect_handler(
+      disconnect_loop.QuitClosure());
+  gesture_source.handler()->OnTapGesture();
+  disconnect_loop.Run();
+
+  // The main frame's renderer must still be responsive.
+  EXPECT_EQ(true, content::EvalJs(web_contents_.get(),
+                                  "document.querySelector('iframe') === null"));
+}
+
 }  // namespace chromecast
Loading diff…

Original Bug Report

reported by [email protected]

Potential Use-After-Free Write in Chromecast CastBinding Subclasses via Synchronous Subframe Detach

Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: Multiple Chromecast CastBinding renderer subclasses synchronously invoke page-supplied JavaScript callbacks and subsequently write to their member variables without checking if the instance is still alive. If the callback synchronously detaches its own subframe, the CastBinding instance self-deletes, leading to a heap use-after-free (UAF) write.

Affected files:

  • chromecast/renderer/cast_window_manager_bindings.cc
  • chromecast/renderer/settings_ui_bindings.cc
  • chromecast/renderer/cast_demo_bindings.cc

Estimated timestamp from git blame: 2021-10-12

Detailed Description

There is a potential Use-After-Free (UAF) write vulnerability in several CastBinding subclasses (CastWindowManagerBindings, SettingsUiBindings, and CastDemoBindings) in the Chromecast renderer process.

All CastBinding subclasses are content::RenderFrameObservers that manage their own lifetime. They synchronously self-delete when their associated RenderFrame is destroyed:

// chromecast/renderer/native_bindings_helper.cc
void CastBinding::OnDestruct() {
  delete this;
}

When page-supplied JavaScript registers a callback via these native bindings, the binding saves it as a v8::UniquePersistent<v8::Function>. Upon certain browser events or Mojo triggers, the binding retrieves this persistent handle and synchronously invokes the callback via v8::Function::Call().

However, there are at least five locations in these subclasses where a member of this is modified after the synchronous Call() execution completes without a liveness check. If the running JavaScript callback synchronously detaches its own subframe (e.g., via frameElement.remove()), the RenderFrame is destroyed, which synchronously calls OnDestruct() and deletes the CastBinding instance. When the execution returns to the C++ caller, the subsequent member write results in a heap UAF write.

Vulnerable Instances

  1. CastWindowManagerBindings::OnBackGesture In chromecast/renderer/cast_window_manager_bindings.cc:

    auto result = handler->Call(context, context->Global(), 0, nullptr);
    on_back_gesture_callback_ =
        v8::UniquePersistent<v8::Function>(isolate, handler); // UAF write
    
  2. CastWindowManagerBindings::InvokeV8Callback (Both overloads) In chromecast/renderer/cast_window_manager_bindings.cc:

    v8::MaybeLocal<v8::Value> maybe_result =
        handler->Call(context, context->Global(), args.size(), args.data());
    *callback_function = v8::UniquePersistent<v8::Function>(isolate, handler); // UAF write
    
  3. CastDemoBindings::VolumeChanged In chromecast/renderer/cast_demo_bindings.cc:

    v8::MaybeLocal<v8::Value> maybe_result =
        handler->Call(context, context->Global(), args.size(), args.data());
    volume_change_handler_ = v8::UniquePersistent<v8::Function>(isolate, handler); // UAF write
    
  4. SettingsUiBindings::HandleSideSwipe In chromecast/renderer/settings_ui_bindings.cc:

    v8::MaybeLocal<v8::Value> maybe_result =
        handler->Call(context, context->Global(), args.size(), args.data());
    side_swipe_handler_ = v8::UniquePersistent<v8::Function>(isolate, handler); // UAF write
    
  5. SettingsUiBindings::SendPlatformInfo In chromecast/renderer/settings_ui_bindings.cc:

    v8::MaybeLocal<v8::Value> maybe_result =
        handler->Call(context, context->Global(), args.size(), args.data());
    platform_info_handler_ = v8::UniquePersistent<v8::Function>(isolate, handler); // UAF write
    

    Additionally, if triggered from JS via SetPlatformInfoHandler():

    if (!pending_platform_info_json_.empty()) {
      SendPlatformInfo(pending_platform_info_json_);
      pending_platform_info_json_.clear(); // Second UAF write on freed std::string
    }
    

Potential Trigger Steps

Note: These are suggested steps from manual code review. Our tooling does not currently have the capability to run code to confirm a working Proof of Concept.

  1. Load a same-origin subframe in a Cast application environment that has feature flags (like system gestures or settings UI Mojo) enabled.
  2. In the subframe’s JavaScript context, register a callback with the binding interface, such as:
    cast.__platform__.windowManager.onTapGesture(() => {
      frameElement.remove(); // Synchronously detaches subframe and deletes the binding
    });
    
  3. Trigger the corresponding event (e.g., simulating a tap gesture or calling a Gin-bound method that leads to the callback execution).
  4. During the synchronous callback execution, frameElement.remove() will trigger RenderFrameImpl::FrameDetached() -> delete this, which in turn triggers CastBinding::OnDestruct() -> delete this on the binding subclass instance.
  5. When V8 returns control to C++, the subclass code attempts to update its member variables (e.g., moving v8::UniquePersistent or calling std::string::clear()), corrupting the freed memory space.

Suggested Fix

To safely resolve this, use a weak pointer liveness check before accessing or modifying any member variables of the CastBinding subclass after returning from synchronous JavaScript execution.

Add a base::WeakPtrFactory to the binding subclasses and modify the callbacks to check the liveness of this:

base::WeakPtr<CastWindowManagerBindings> weak_this = weak_factory_.GetWeakPtr();

v8::MaybeLocal<v8::Value> maybe_result =
    handler->Call(context, context->Global(), 0, nullptr);

if (!weak_this) {
  return; // The frame and binding have been deleted
}

*callback_function = v8::UniquePersistent<v8::Function>(isolate, handler);

Evaluated with Chrome root at commit: 3947e01999a53d4e2382e39736cb79d79c7dffcf


Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:

  • If you are familiar with the severity guidelines, you may adjust the severity.
  • If this is a false positive, and there’s no work to be done, please close as WAI.
  • If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.

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