Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Actor
DescriptionUse after free in Actor
ComponentActor
Bug ClassUAF
Tracker506150628
Fix commit7907fe19b3ed (chromium/src) +293/-49
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-02

Changed Functions

FunctionChangeNotes
ActorUafRegressionBrowserTest
chrome/browser/actor/tools/uaf_regression_browsertest.cc
modified
if
chrome/browser/actor/tools/uaf_regression_browsertest.cc
modified

Files Changed

  • chrome/browser/actor/BUILD.gn
  • chrome/browser/actor/tools/select_tool_browsertest.cc
  • chrome/browser/actor/tools/uaf_regression_browsertest.cc
From 7907fe19b3eda099b6b9a78434c56fd6edb637f5 Mon Sep 17 00:00:00 2001
From: Andrew Paseltiner <[email protected]>
Date: Wed, 29 Apr 2026 08:26:00 -0700
Subject: [PATCH] [Glic] Fix UAF and crashes in actor tools during synchronous frame detachment

This CL fixes Use-After-Free (UAF) vulnerabilities and potential crashes
in several Glic actor tools (Click, Type, and DragAndRelease) that occur
when a page script synchronously detaches a frame or modifies the DOM in
response to an input event.

When these tools dispatch input events (like MouseDown or KeyDown) to
the renderer, the page may execute script that results in the
destruction of the tool object itself or its associated resources. To
handle this safely, we now:

- Use base::WeakPtr to check if the tool object is still alive after
  each synchronous event dispatch.
- Return early if the tool has been destroyed, preventing access to
  freed memory.
- Ensure that the WebWidget is re-validated where necessary.

Regression tests are added in uaf_regression_browsertest.cc using a
nested "ABA" iframe structure to reliably trigger synchronous
detachment. This suite covers the new fixes for Click, Type, and
DragAndRelease.

Bug: 506377279
Change-Id: Ie0753f925b7264a127aa1e37d7e41bcabb25db99
Fixed: 506150628
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7800758
Commit-Queue: Andrew Paseltiner <[email protected]>
Reviewed-by: David Bokan <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1622471}
---

diff --git a/chrome/browser/actor/BUILD.gn b/chrome/browser/actor/BUILD.gn
index 9bacadda..4b6e449f5 100644
--- a/chrome/browser/actor/BUILD.gn
+++ b/chrome/browser/actor/BUILD.gn
@@ -460,6 +460,7 @@
       "tools/tab_management_tool_browsertest.cc",
       "tools/tool_agnostic_browsertest.cc",
       "tools/type_tool_browsertest.cc",
+      "tools/uaf_regression_browsertest.cc",
       "tools/wait_tool_browsertest.cc",
       "tools/window_management_tool_browsertest.cc",
     ]
diff --git a/chrome/browser/actor/tools/select_tool_browsertest.cc b/chrome/browser/actor/tools/select_tool_browsertest.cc
index 4df0768..e52ff28b 100644
--- a/chrome/browser/actor/tools/select_tool_browsertest.cc
+++ b/chrome/browser/actor/tools/select_tool_browsertest.cc
@@ -408,51 +408,5 @@
             "delta");
 }
 
-// Regression test for UAF in Glic actor tools.
-// See crbug.com/506377279.
-IN_PROC_BROWSER_TEST_F(ActorSelectToolBrowserTest,
-                       SelectTool_HandlesSynchronousFrameDetachment) {
-  GURL outer_url = embedded_https_test_server().GetURL(
-      "a.test", "/actor/select_tool_uaf_outer.html");
-  GURL mid_url = embedded_https_test_server().GetURL(
-      "b.test", "/actor/select_tool_uaf_mid.html");
-  GURL inner_url = embedded_https_test_server().GetURL(
-      "a.test", "/actor/select_tool_uaf_inner.html");
-
-  ASSERT_TRUE(content::NavigateToURL(web_contents(), outer_url));
-  content::WaitForLoadStop(web_contents());
-
-  // Set mid iframe src
-  ASSERT_TRUE(content::ExecJs(
-      web_contents(),
-      content::JsReplace("document.getElementById('mid').src = $1", mid_url)));
-  content::WaitForLoadStop(web_contents());
-
-  content::RenderFrameHost* mid_rfh = content::ChildFrameAt(main_frame(), 0);
-  ASSERT_TRUE(mid_rfh);
-
-  // Set inner iframe src
-  ASSERT_TRUE(content::ExecJs(
-      mid_rfh, content::JsReplace("document.getElementById('inner').src = $1",
-                                  inner_url)));
-  content::WaitForLoadStop(web_contents());
-
-  content::RenderFrameHost* inner_rfh = content::ChildFrameAt(mid_rfh, 0);
-  ASSERT_TRUE(inner_rfh);
-
-  const std::string select_id = "#s";
-  const int32_t select_dom_node_id =
-      GetDOMNodeId(*inner_rfh, select_id).value();
-
-  // Trigger SelectTool on the inner frame.
-  std::unique_ptr<ToolRequest> action =
-      MakeSelectRequest(*inner_rfh, select_dom_node_id, "b");
-  ActResultFuture result;
-  actor_task().Act(ToRequestList(action), result.GetCallback());
-
-  // With the fix, this should not crash the renderer.
-  ASSERT_TRUE(result.Wait());
-}
-
 }  // namespace
 }  // namespace actor
diff --git a/chrome/browser/actor/tools/uaf_regression_browsertest.cc b/chrome/browser/actor/tools/uaf_regression_browsertest.cc
new file mode 100644
index 0000000..dd032bba
--- /dev/null
+++ b/chrome/browser/actor/tools/uaf_regression_browsertest.cc
@@ -0,0 +1,153 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "base/test/test_future.h"
+#include "chrome/browser/actor/actor_test_util.h"
+#include "chrome/browser/actor/tools/tool_request.h"
+#include "chrome/browser/actor/tools/tools_test_util.h"
+#include "chrome/common/actor.mojom.h"
+#include "chrome/common/actor/action_result.h"
+#include "content/public/test/browser_test.h"
+#include "content/public/test/browser_test_utils.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "ui/gfx/geometry/point.h"
+#include "ui/gfx/geometry/point_conversions.h"
+
+namespace actor {
+
+namespace {
+
+using base::test::TestFuture;
+using content::ChildFrameAt;
+using content::ExecJs;
+using content::GetDOMNodeId;
+using content::RenderFrameHost;
+using ActResultFuture = TestFuture<std::vector<ActionResultWithLatencyInfo>>;
+
+class ActorUafRegressionBrowserTest : public ActorToolsTest {
+ public:
+  void SetUpOnMainThread() override {
+    ActorToolsTest::SetUpOnMainThread();
+    ASSERT_TRUE(embedded_test_server()->Start());
+    embedded_https_test_server().SetSSLConfig(
+        net::EmbeddedTestServer::CERT_TEST_NAMES);
+    ASSERT_TRUE(embedded_https_test_server().Start());
+  }
+
+  void SetupAbaFrames(const std::string& inner_path) {
+    GURL outer_url =
+        embedded_https_test_server().GetURL("a.test", "/actor/uaf_outer.html");
+    GURL mid_url =
+        embedded_https_test_server().GetURL("b.test", "/actor/uaf_mid.html");
+    GURL inner_url = embedded_https_test_server().GetURL("a.test", inner_path);
+
+    ASSERT_TRUE(content::NavigateToURL(web_contents(), outer_url));
+    content::WaitForLoadStop(web_contents());
+
+    // Set mid iframe src
+    ASSERT_TRUE(
+        ExecJs(web_contents(),
+               content::JsReplace("document.getElementById('mid').src = $1",
+                                  mid_url)));
+    content::WaitForLoadStop(web_contents());
+
+    RenderFrameHost* mid_rfh = ChildFrameAt(main_frame(), 0);
+    ASSERT_TRUE(mid_rfh);
+
+    // Set inner iframe src
+    ASSERT_TRUE(ExecJs(
+        mid_rfh, content::JsReplace("document.getElementById('inner').src = $1",
+                                    inner_url)));
+    content::WaitForLoadStop(web_contents());
+  }
+
+  RenderFrameHost* GetInnerRfh() {
+    RenderFrameHost* mid_rfh = ChildFrameAt(main_frame(), 0);
+    if (!mid_rfh) {
+      return nullptr;
+    }
+    return ChildFrameAt(mid_rfh, 0);
+  }
+};
+
+// Regression test for UAF in Glic actor tools.
+// See crbug.com/506150628.
+IN_PROC_BROWSER_TEST_F(ActorUafRegressionBrowserTest,
+                       ClickTool_HandlesSynchronousFrameDetachment) {
+  SetupAbaFrames("/actor/click_tool_uaf_inner.html");
+  RenderFrameHost* inner_rfh = GetInnerRfh();
+  ASSERT_TRUE(inner_rfh);
+
+  const int32_t target_id = GetDOMNodeId(*inner_rfh, "#target").value();
+
+  std::unique_ptr<ToolRequest> action = MakeClickRequest(*inner_rfh, target_id);
+  ActResultFuture result;
+  actor_task().Act(ToRequestList(action), result.GetCallback());
+
+  // This should not crash the renderer.
+  ASSERT_TRUE(result.Wait());
+}
+
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/chrome/browser/actor/tools/select_tool_browsertest.cc b/chrome/browser/actor/tools/select_tool_browsertest.cc
index 4df0768..e52ff28b 100644
--- a/chrome/browser/actor/tools/select_tool_browsertest.cc
+++ b/chrome/browser/actor/tools/select_tool_browsertest.cc
@@ -408,51 +408,5 @@
             "delta");
 }
 
-// Regression test for UAF in Glic actor tools.
-// See crbug.com/506377279.
-IN_PROC_BROWSER_TEST_F(ActorSelectToolBrowserTest,
-                       SelectTool_HandlesSynchronousFrameDetachment) {
-  GURL outer_url = embedded_https_test_server().GetURL(
-      "a.test", "/actor/select_tool_uaf_outer.html");
-  GURL mid_url = embedded_https_test_server().GetURL(
-      "b.test", "/actor/select_tool_uaf_mid.html");
-  GURL inner_url = embedded_https_test_server().GetURL(
-      "a.test", "/actor/select_tool_uaf_inner.html");
-
-  ASSERT_TRUE(content::NavigateToURL(web_contents(), outer_url));
-  content::WaitForLoadStop(web_contents());
-
-  // Set mid iframe src
-  ASSERT_TRUE(content::ExecJs(
-      web_contents(),
-      content::JsReplace("document.getElementById('mid').src = $1", mid_url)));
-  content::WaitForLoadStop(web_contents());
-
-  content::RenderFrameHost* mid_rfh = content::ChildFrameAt(main_frame(), 0);
-  ASSERT_TRUE(mid_rfh);
-
-  // Set inner iframe src
-  ASSERT_TRUE(content::ExecJs(
-      mid_rfh, content::JsReplace("document.getElementById('inner').src = $1",
-                                  inner_url)));
-  content::WaitForLoadStop(web_contents());
-
-  content::RenderFrameHost* inner_rfh = content::ChildFrameAt(mid_rfh, 0);
-  ASSERT_TRUE(inner_rfh);
-
-  const std::string select_id = "#s";
-  const int32_t select_dom_node_id =
-      GetDOMNodeId(*inner_rfh, select_id).value();
-
-  // Trigger SelectTool on the inner frame.
-  std::unique_ptr<ToolRequest> action =
-      MakeSelectRequest(*inner_rfh, select_dom_node_id, "b");
-  ActResultFuture result;
-  actor_task().Act(ToRequestList(action), result.GetCallback());
-
-  // With the fix, this should not crash the renderer.
-  ASSERT_TRUE(result.Wait());
-}
-
 }  // namespace
 }  // namespace actor
diff --git a/chrome/browser/actor/tools/uaf_regression_browsertest.cc b/chrome/browser/actor/tools/uaf_regression_browsertest.cc
new file mode 100644
index 0000000..dd032bba
--- /dev/null
+++ b/chrome/browser/actor/tools/uaf_regression_browsertest.cc
@@ -0,0 +1,153 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "base/test/test_future.h"
+#include "chrome/browser/actor/actor_test_util.h"
+#include "chrome/browser/actor/tools/tool_request.h"
+#include "chrome/browser/actor/tools/tools_test_util.h"
+#include "chrome/common/actor.mojom.h"
+#include "chrome/common/actor/action_result.h"
+#include "content/public/test/browser_test.h"
+#include "content/public/test/browser_test_utils.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "ui/gfx/geometry/point.h"
+#include "ui/gfx/geometry/point_conversions.h"
+
+namespace actor {
+
+namespace {
+
+using base::test::TestFuture;
+using content::ChildFrameAt;
+using content::ExecJs;
+using content::GetDOMNodeId;
+using content::RenderFrameHost;
+using ActResultFuture = TestFuture<std::vector<ActionResultWithLatencyInfo>>;
+
+class ActorUafRegressionBrowserTest : public ActorToolsTest {
+ public:
+  void SetUpOnMainThread() override {
+    ActorToolsTest::SetUpOnMainThread();
+    ASSERT_TRUE(embedded_test_server()->Start());
+    embedded_https_test_server().SetSSLConfig(
+        net::EmbeddedTestServer::CERT_TEST_NAMES);
+    ASSERT_TRUE(embedded_https_test_server().Start());
+  }
+
+  void SetupAbaFrames(const std::string& inner_path) {
+    GURL outer_url =
+        embedded_https_test_server().GetURL("a.test", "/actor/uaf_outer.html");
+    GURL mid_url =
+        embedded_https_test_server().GetURL("b.test", "/actor/uaf_mid.html");
+    GURL inner_url = embedded_https_test_server().GetURL("a.test", inner_path);
+
+    ASSERT_TRUE(content::NavigateToURL(web_contents(), outer_url));
+    content::WaitForLoadStop(web_contents());
+
+    // Set mid iframe src
+    ASSERT_TRUE(
+        ExecJs(web_contents(),
+               content::JsReplace("document.getElementById('mid').src = $1",
+                                  mid_url)));
+    content::WaitForLoadStop(web_contents());
+
+    RenderFrameHost* mid_rfh = ChildFrameAt(main_frame(), 0);
+    ASSERT_TRUE(mid_rfh);
+
+    // Set inner iframe src
+    ASSERT_TRUE(ExecJs(
+        mid_rfh, content::JsReplace("document.getElementById('inner').src = $1",
+                                    inner_url)));
+    content::WaitForLoadStop(web_contents());
+  }
+
+  RenderFrameHost* GetInnerRfh() {
+    RenderFrameHost* mid_rfh = ChildFrameAt(main_frame(), 0);
+    if (!mid_rfh) {
+      return nullptr;
+    }
+    return ChildFrameAt(mid_rfh, 0);
+  }
+};
+
+// Regression test for UAF in Glic actor tools.
+// See crbug.com/506150628.
+IN_PROC_BROWSER_TEST_F(ActorUafRegressionBrowserTest,
+                       ClickTool_HandlesSynchronousFrameDetachment) {
+  SetupAbaFrames("/actor/click_tool_uaf_inner.html");
+  RenderFrameHost* inner_rfh = GetInnerRfh();
+  ASSERT_TRUE(inner_rfh);
+
+  const int32_t target_id = GetDOMNodeId(*inner_rfh, "#target").value();
+
+  std::unique_ptr<ToolRequest> action = MakeClickRequest(*inner_rfh, target_id);
+  ActResultFuture result;
+  actor_task().Act(ToRequestList(action), result.GetCallback());
+
+  // This should not crash the renderer.
+  ASSERT_TRUE(result.Wait());
+}
+
+// Regression test for UAF in Glic actor tools.
+// See crbug.com/506150628.
+IN_PROC_BROWSER_TEST_F(ActorUafRegressionBrowserTest,
+                       TypeTool_HandlesSynchronousFrameDetachment) {
+  SetupAbaFrames("/actor/type_tool_uaf_inner.html");
+  RenderFrameHost* inner_rfh = GetInnerRfh();
+  ASSERT_TRUE(inner_rfh);
+
+  const int32_t target_id = GetDOMNodeId(*inner_rfh, "#target").value();
+
+  std::unique_ptr<ToolRequest> action = MakeTypeRequest(
+      *inner_rfh, target_id, "hello", /* follow_by_enter= */ false);
+  ActResultFuture result;
+  actor_task().Act(ToRequestList(action), result.GetCallback());
+
+  // This should not crash the renderer.
+  ASSERT_TRUE(result.Wait());
+}
+
+// Regression test for UAF in Glic actor tools.
+// See crbug.com/506150628.
+IN_PROC_BROWSER_TEST_F(ActorUafRegressionBrowserTest,
+                       DragAndReleaseTool_HandlesSynchronousFrameDetachment) {
+  SetupAbaFrames("/actor/drag_and_release_tool_uaf_inner.html");
+  RenderFrameHost* inner_rfh = GetInnerRfh();
+  ASSERT_TRUE(inner_rfh);
+
+  gfx::RectF bounds = GetBoundingClientRect(*inner_rfh, "#target");
+  gfx::Point from_point = gfx::ToRoundedPoint(bounds.CenterPoint());
+  gfx::Point to_point = from_point + gfx::Vector2d(100, 100);
+
+  // Drag from target to somewhere else.
+  std::unique_ptr<ToolRequest> action =
+      MakeDragAndReleaseRequest(*active_tab(), from_point, to_point);
+  ActResultFuture result;
+  actor_task().Act(ToRequestList(action), result.GetCallback());
+
+  // This should not crash the renderer.
+  ASSERT_TRUE(result.Wait());
+}
+
+// Regression test for UAF in Glic actor tools.
+// See crbug.com/506377279.
+IN_PROC_BROWSER_TEST_F(ActorUafRegressionBrowserTest,
+                       SelectTool_HandlesSynchronousFrameDetachment) {
+  SetupAbaFrames("/actor/select_tool_uaf_inner.html");
+  RenderFrameHost* inner_rfh = GetInnerRfh();
+  ASSERT_TRUE(inner_rfh);
+
+  const int32_t target_id = GetDOMNodeId(*inner_rfh, "#s").value();
+
+  std::unique_ptr<ToolRequest> action =
+      MakeSelectRequest(*inner_rfh, target_id, "b");
+  ActResultFuture result;
+  actor_task().Act(ToRequestList(action), result.GetCallback());
+
+  // This should not crash the renderer.
+  ASSERT_TRUE(result.Wait());
+}
+
+}  // namespace
+}  // namespace actor
diff --git a/chrome/test/data/actor/click_tool_uaf_inner.html b/chrome/test/data/actor/click_tool_uaf_inner.html
new file mode 100644
index 0000000..86ef17c1
--- /dev/null
+++ b/chrome/test/data/actor/click_tool_uaf_inner.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html>
+<body>
+  <div id="target" style="width: 100px; height: 100px; background: red;"></div>
+  <script>
+    const target = document.getElementById('target');
+    target.addEventListener('mousedown', () => {
+      // Remove the mid iframe from the top window.
+      // Since mid is cross-origin, removing it synchronously detaches this frame.
+      parent.parent.document.getElementById('mid').remove();
+    });
+  </script>
+</body>
+</html>
diff --git a/chrome/test/data/actor/drag_and_release_tool_uaf_inner.html b/chrome/test/data/actor/drag_and_release_tool_uaf_inner.html
new file mode 100644
index 0000000..19cfb22
--- /dev/null
+++ b/chrome/test/data/actor/drag_and_release_tool_uaf_inner.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html>
+<body>
+  <div id="target" style="width: 100px; height: 100px; background: blue;"></div>
+  <script>
+    const target = document.getElementById('target');
+    target.addEventListener('mousedown', () => {
+      parent.parent.document.getElementById('mid').remove();
+    });
+  </script>
+</body>
+</html>
diff --git a/chrome/test/data/actor/select_tool_uaf_mid.html b/chrome/test/data/actor/select_tool_uaf_mid.html
deleted file mode 100644
index 39847961..0000000
--- a/chrome/test/data/actor/select_tool_uaf_mid.html
+++ /dev/null
@@ -1,6 +0,0 @@
-<!DOCTYPE html>
-<html>
-<body>
-  <iframe id="inner" width="400" height="400"></iframe>
-</body>
-</html>
diff --git a/chrome/test/data/actor/select_tool_uaf_outer.html b/chrome/test/data/actor/select_tool_uaf_outer.html
deleted file mode 100644
index 8a0f98a..0000000
--- a/chrome/test/data/actor/select_tool_uaf_outer.html
+++ /dev/null
@@ -1,6 +0,0 @@
-<!DOCTYPE html>
-<html>
-<body>
-  <iframe id="mid" width="500" height="500"></iframe>
-</body>
-</html>
diff --git a/chrome/test/data/actor/type_tool_uaf_inner.html b/chrome/test/data/actor/type_tool_uaf_inner.html
new file mode 100644
index 0000000..c55a208b
--- /dev/null
+++ b/chrome/test/data/actor/type_tool_uaf_inner.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html>
+<body>
+  <input id="target" type="text">
+  <script>
+    const target = document.getElementById('target');
+    target.addEventListener('keydown', () => {
+      parent.parent.document.getElementById('mid').remove();
+    });
+  </script>
+</body>
+</html>
diff --git a/chrome/test/data/actor/uaf_mid.html b/chrome/test/data/actor/uaf_mid.html
new file mode 100644
index 0000000..39847961
--- /dev/null
+++ b/chrome/test/data/actor/uaf_mid.html
... (truncated)
Loading diff…

Original Bug Report

reported by [email protected]

UAF Write in Glic Actor Dispatchers via Synchronous Frame Detachment

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 without the Chrome Security team. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: A Use-After-Free (UAF) vulnerability exists in the Glic Actor’s ClickDispatcher and TypeTool when dispatching input events. The synchronous HandleInputEvent() call can trigger a JavaScript listener that detaches the local root frame, synchronously destroying the tool’s heap allocation. When execution resumes, it uses a dangling this pointer to write to the freed chunk, creating an exploitable primitive that bypasses MiraclePtr.

Affected files:

  • chrome/renderer/actor/click_dispatcher.cc
  • chrome/renderer/actor/click_tool.h
  • chrome/renderer/actor/tool_executor.cc
  • chrome/renderer/actor/type_tool.h

Estimated timestamp from git blame: 2025-12-10

Summary

A Use-After-Free (UAF) write vulnerability occurs in ClickDispatcher and TypeTool within the sandboxed renderer process. These classes dispatch user input events to the DOM using widget->HandleInputEvent(). This call synchronously executes the target page’s JavaScript event listeners. If a malicious listener executes script to detach the RenderFrame hosting the actor (e.g., by removing an out-of-process iframe parent), the entire ownership chain—ChromeRenderFrameObserver -> ToolExecutor -> ClickTool/TypeTool—is synchronously destroyed.

When HandleInputEvent() returns, execution resumes in ClickDispatcher::DoMouseDown (or equivalent TypeTool functions) using a dangling this pointer. This leads to a deterministic heap write of the WebMouseEvent into freed memory, providing a powerful primitive for renderer Remote Code Execution (RCE).

Root Cause Analysis

In chrome/renderer/actor/click_dispatcher.cc, the DoMouseDown method initiates a click:

  blink::WebInputEventResult result = widget->HandleInputEvent(
      blink::WebCoalescedInputEvent(mouse_down, ui::LatencyInfo()));
  // execution resumes here after synchronous JS
  
  if (result == blink::WebInputEventResult::kHandledSuppressed) {
    Finish(...);
    return;
  }

  // UAF WRITE: copies ~136 bytes into the potentially freed tool allocation
  mouse_up_event_ = mouse_down;

A similar pattern exists in TypeTool::SimulateKeyPress and TypeTool::CreateAndDispatchKeyEvent, where journal_ and task_id_ are accessed after the input dispatch.

Crucially, MiraclePtr (BackupRefPtr) does not mitigate this. ClickDispatcher is stored inline as a std::optional inside ClickTool. The only BRP-managed pointer to the ClickTool allocation is the raw_ref<const ToolBase> tool_ stored inside the ClickDispatcher. When the frame is detached, the ClickTool destructor is invoked, destroying the ClickDispatcher and its raw_ref. This drives the BRP reference count to zero before PartitionAlloc frees the chunk. The chunk is released immediately, skipping the BRP quarantine, allowing an attacker to reclaim it deterministically via a heap spray inside their synchronous JS listener.

Potential Reproduction Steps

  1. Setup a page with an A-B-A frame hierarchy (Top-A -> cross-origin iframe B -> same-process local-root iframe inner-A).
  2. Register a mousedown listener on an element in inner-A.
  3. The attacker triggers the Glic Actor to click the element.
  4. When DoMouseDown dispatches the event, the listener synchronously fires.
  5. The listener executes window.top.document.getElementById('b').remove();.
  6. This synchronously detaches inner-A, destroying the ChromeRenderFrameObserver, ToolExecutor, and the ClickTool/ClickDispatcher.
  7. The listener performs a heap spray to reclaim the freed ClickTool memory chunk.
  8. The widget->HandleInputEvent() call returns, and DoMouseDown executes mouse_up_event_ = mouse_down;, overwriting the reclaimed chunk with the attacker-influenced WebMouseEvent struct.

Proposed Fix

The affected dispatcher methods should use base::WeakPtr to check if the instance survived the synchronous HandleInputEvent call.

  base::WeakPtr<ClickDispatcher> weak_this = weak_ptr_factory_.GetWeakPtr();
  blink::WebInputEventResult result = widget->HandleInputEvent(
      blink::WebCoalescedInputEvent(mouse_down, ui::LatencyInfo()));
      
  if (!weak_this) {
    return;
  }

This pattern should be applied to ClickDispatcher::ClickDispatcher, ClickDispatcher::DoMouseDown, ClickDispatcher::DoMouseUpImpl, TypeTool::CreateAndDispatchKeyEvent, and KeyDispatcher::CreateAndDispatchKeyEvent.

Evaluated with Chrome root at commit: 3acbde3302da0cb19488c22c0eb007c791207b4b


Results 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.

Raised in root component due to access or custom field issues on 1707859

View on issue tracker