CVE-2026-113087
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchrome/renderer/actor/click_dispatcher.cc |
modified | |
ClickDispatcherchrome/renderer/actor/click_dispatcher.h |
modified |
Files Changed
chrome/renderer/actor/click_tool.ccchrome/renderer/actor/click_dispatcher.ccchrome/renderer/actor/type_tool.cccomponents/test/data/actor/type_tool_uaf_mousemove_inner.htmlcomponents/test/data/actor/click_tool_uaf_mousemove_inner.htmlchrome/browser/actor/tools/uaf_regression_browsertest.ccchrome/renderer/actor/click_dispatcher.h
Patch
From b36c618599df1c588a7485ace1f89e82a8cf2ee5 Mon Sep 17 00:00:00 2001
From: Andrew Paseltiner <apaseltiner@chromium.org>
Date: Mon, 21 Sep 2026 15:01:37 -0700
Subject: [PATCH] [Actor] Prevent UAF during ClickDispatcher initialization
ClickDispatcher previously dispatched synchronous mousemove events
directly from its constructor. When constructed in-place inside
std::optional (in ClickTool and TypeTool), reentrant destruction of the
owning tool caused by synchronous frame detachment occurred before the
optional was engaged. As a result, ~ClickDispatcher() was not invoked,
its WeakPtrFactory was not invalidated, and the constructor proceeded to
access freed memory upon returning from event dispatch.
This CL resolves the issue by:
1. Keeping ClickDispatcher's constructor side-effect-free.
2. Introducing an explicit Start() method to begin event dispatch
once the dispatcher is already engaged in its owner.
3. Adding regression tests in ActorUafRegressionBrowserTest for
mousemove synchronous detachment in both ClickTool and TypeTool.
Fixed: 558301759
Change-Id: Icc6209a9183fade8c773da5b421d96def84a5469
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8435515
Reviewed-by: Kevin McNee <mcnee@chromium.org>
Commit-Queue: Andrew Paseltiner <apaseltiner@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1702088}
---
diff --git a/chrome/renderer/actor/click_dispatcher.cc b/chrome/renderer/actor/click_dispatcher.cc
index 843c9434..6a52886d 100644
--- a/chrome/renderer/actor/click_dispatcher.cc
+++ b/chrome/renderer/actor/click_dispatcher.cc
@@ -42,8 +42,16 @@
const ResolvedTarget& target,
const ToolBase& tool,
base::OnceCallback<void(mojom::ActionResultPtr)> on_complete)
- : target_(target), tool_(tool), on_complete_(std::move(on_complete)) {
- WebWidget* widget = target.GetWidget(*tool_);
+ : button_(button),
+ count_(count),
+ target_(target),
+ tool_(tool),
+ on_complete_(std::move(on_complete)) {}
+
+ClickDispatcher::~ClickDispatcher() = default;
+
+void ClickDispatcher::Start() {
+ WebWidget* widget = target_.GetWidget(*tool_);
CHECK(widget);
if (base::FeatureList::IsEnabled(features::kGlicActorMoveBeforeClick)) {
WebMouseEvent mouse_move(WebInputEvent::Type::kMouseMove,
@@ -51,9 +59,9 @@
ui::EventTimeForNow());
// No button for move
mouse_move.button = WebMouseEvent::Button::kNoButton;
- mouse_move.SetPositionInWidget(target.widget_point);
+ mouse_move.SetPositionInWidget(target_.widget_point);
mouse_move.SetPositionInScreen(
- target.widget_point +
+ target_.widget_point +
gfx::Vector2dF(widget->ViewRect().OffsetFromOrigin()));
// Mouse move is considered optional, so we don't check this result.
@@ -69,15 +77,13 @@
base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask(
FROM_HERE,
base::BindOnce(&ClickDispatcher::DoMouseDown,
- weak_ptr_factory_.GetWeakPtr(), button, count, target),
+ weak_ptr_factory_.GetWeakPtr()),
delay);
} else {
- DoMouseDown(button, count, target);
+ DoMouseDown();
}
}
-ClickDispatcher::~ClickDispatcher() = default;
-
void ClickDispatcher::Cancel() {
weak_ptr_factory_.InvalidateWeakPtrs();
on_complete_.Reset();
@@ -88,10 +94,8 @@
}
}
-void ClickDispatcher::DoMouseDown(WebMouseEvent::Button button,
- int count,
- const ResolvedTarget& target) {
- WebWidget* widget = target.GetWidget(*tool_);
+void ClickDispatcher::DoMouseDown() {
+ WebWidget* widget = target_.GetWidget(*tool_);
if (!widget) {
Finish(MakeResult(mojom::ActionResultCode::kFrameWentAway,
/*requires_page_stabilization=*/true,
@@ -101,12 +105,12 @@
WebMouseEvent mouse_down(WebInputEvent::Type::kMouseDown,
WebInputEvent::kNoModifiers, ui::EventTimeForNow());
- mouse_down.button = button;
- mouse_down.click_count = count;
- mouse_down.SetPositionInWidget(target.widget_point);
+ mouse_down.button = button_;
+ mouse_down.click_count = count_;
+ mouse_down.SetPositionInWidget(target_.widget_point);
gfx::PointF screen_point =
- target.widget_point +
+ target_.widget_point +
gfx::Vector2dF(widget->ViewRect().OffsetFromOrigin());
mouse_down.SetPositionInScreen(screen_point);
mouse_down.UpdateEventModifiersToMatchButton();
diff --git a/chrome/renderer/actor/click_dispatcher.h b/chrome/renderer/actor/click_dispatcher.h
index 06aa46fd..8a51b6e 100644
--- a/chrome/renderer/actor/click_dispatcher.h
+++ b/chrome/renderer/actor/click_dispatcher.h
@@ -25,8 +25,6 @@
// The `on_complete` caller may safely destroy this ClickDispatcher.
class ClickDispatcher {
public:
- // Starts the sequence of mouse events upon creation.
- //
// The passed in `tool` must own this instance.
//
// `on_complete` is always called asynchronously, but isn't invoked if
@@ -38,15 +36,16 @@
base::OnceCallback<void(mojom::ActionResultPtr)> on_complete);
~ClickDispatcher();
+ // Starts the sequence of mouse events.
+ void Start();
+
// Cancels future dispatching, with the exception that if a mouse button is in
// a down state, mouse up will be synchronously dispatched.
void Cancel();
private:
// Clicks the mouse down, and calls DoMouseUp() asynchronously after a delay.
- void DoMouseDown(blink::WebMouseEvent::Button button,
- int count,
- const ResolvedTarget& target);
+ void DoMouseDown();
// Calls DoMouseUpImpl(), then calls Finish(). Used by Cancel() so it can
// Finish() with a different result.
@@ -59,6 +58,9 @@
// called, or if Cancel() has been called.
void Finish(mojom::ActionResultPtr result);
+ const blink::WebMouseEvent::Button button_;
+ const int count_;
+
// The target to click on.
const ResolvedTarget target_;
diff --git a/chrome/renderer/actor/click_tool.cc b/chrome/renderer/actor/click_tool.cc
index c33d856..c346c39 100644
--- a/chrome/renderer/actor/click_tool.cc
+++ b/chrome/renderer/actor/click_tool.cc
@@ -149,6 +149,7 @@
CHECK(!click_dispatcher_);
click_dispatcher_.emplace(button, click_count, target, *this,
std::move(callback));
+ click_dispatcher_->Start();
}
std::string ClickTool::DebugString() const {
diff --git a/chrome/renderer/actor/type_tool.cc b/chrome/renderer/actor/type_tool.cc
index 310d749..55d1bb77 100644
--- a/chrome/renderer/actor/type_tool.cc
+++ b/chrome/renderer/actor/type_tool.cc
@@ -487,6 +487,7 @@
blink::WebMouseEvent::Button::kLeft, 1, *resolved_target_, *this,
base::BindOnce(&TypeTool::OnFocusingClickComplete,
weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
+ click_dispatcher_->Start();
}
void TypeTool::Cancel() {
diff --git a/chrome/browser/actor/tools/uaf_regression_browsertest.cc b/chrome/browser/actor/tools/uaf_regression_browsertest.cc
index c45d5cf..75607165 100644
--- a/chrome/browser/actor/tools/uaf_regression_browsertest.cc
+++ b/chrome/browser/actor/tools/uaf_regression_browsertest.cc
@@ -86,6 +86,28 @@
// This should not crash the renderer.
ASSERT_TRUE(result.Wait());
+ ExpectErrorResult(result, mojom::ActionResultCode::kFrameWentAway);
+ EXPECT_TRUE(main_frame()->IsRenderFrameLive());
+}
+
+// Regression test for UAF in Glic actor tools.
+// See crbug.com/558301759.
+IN_PROC_BROWSER_TEST_F(ActorUafRegressionBrowserTest,
+ ClickTool_HandlesSynchronousFrameDetachmentOnMouseMove) {
+ SetupAbaFrames("/actor/click_tool_uaf_mousemove_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());
+ ExpectErrorResult(result, mojom::ActionResultCode::kFrameWentAway);
+ EXPECT_TRUE(main_frame()->IsRenderFrameLive());
}
// Regression test for UAF in Glic actor tools.
@@ -108,6 +130,25 @@
}
// Regression test for UAF in Glic actor tools.
+// See crbug.com/558301759.
+IN_PROC_BROWSER_TEST_F(ActorUafRegressionBrowserTest,
+ TypeTool_HandlesSynchronousFrameDetachmentOnMouseMove) {
+ SetupAbaFrames("/actor/type_tool_uaf_mousemove_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) {
diff --git a/components/test/data/actor/click_tool_uaf_mousemove_inner.html b/components/test/data/actor/click_tool_uaf_mousemove_inner.html
new file mode 100644
index 0000000..307160f3
--- /dev/null
+++ b/components/test/data/actor/click_tool_uaf_mousemove_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('mousemove', () => {
+ // 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/components/test/data/actor/type_tool_uaf_mousemove_inner.html b/components/test/data/actor/type_tool_uaf_mousemove_inner.html
new file mode 100644
index 0000000..b9e645de7
--- /dev/null
+++ b/components/test/data/actor/type_tool_uaf_mousemove_inner.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html>
+<body>
+ <input id="target" type="text">
+ <script>
+ const target = document.getElementById('target');
+ target.addEventListener('mousemove', () => {
+ // 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>
Regression Test / PoC
diff --git a/chrome/browser/actor/tools/uaf_regression_browsertest.cc b/chrome/browser/actor/tools/uaf_regression_browsertest.cc
index c45d5cf..75607165 100644
--- a/chrome/browser/actor/tools/uaf_regression_browsertest.cc
+++ b/chrome/browser/actor/tools/uaf_regression_browsertest.cc
@@ -86,6 +86,28 @@
// This should not crash the renderer.
ASSERT_TRUE(result.Wait());
+ ExpectErrorResult(result, mojom::ActionResultCode::kFrameWentAway);
+ EXPECT_TRUE(main_frame()->IsRenderFrameLive());
+}
+
+// Regression test for UAF in Glic actor tools.
+// See crbug.com/558301759.
+IN_PROC_BROWSER_TEST_F(ActorUafRegressionBrowserTest,
+ ClickTool_HandlesSynchronousFrameDetachmentOnMouseMove) {
+ SetupAbaFrames("/actor/click_tool_uaf_mousemove_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());
+ ExpectErrorResult(result, mojom::ActionResultCode::kFrameWentAway);
+ EXPECT_TRUE(main_frame()->IsRenderFrameLive());
}
// Regression test for UAF in Glic actor tools.
@@ -108,6 +130,25 @@
}
// Regression test for UAF in Glic actor tools.
+// See crbug.com/558301759.
+IN_PROC_BROWSER_TEST_F(ActorUafRegressionBrowserTest,
+ TypeTool_HandlesSynchronousFrameDetachmentOnMouseMove) {
+ SetupAbaFrames("/actor/type_tool_uaf_mousemove_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) {
diff --git a/components/test/data/actor/click_tool_uaf_mousemove_inner.html b/components/test/data/actor/click_tool_uaf_mousemove_inner.html
new file mode 100644
index 0000000..307160f3
--- /dev/null
+++ b/components/test/data/actor/click_tool_uaf_mousemove_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('mousemove', () => {
+ // 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/components/test/data/actor/type_tool_uaf_mousemove_inner.html b/components/test/data/actor/type_tool_uaf_mousemove_inner.html
new file mode 100644
index 0000000..b9e645de7
--- /dev/null
+++ b/components/test/data/actor/type_tool_uaf_mousemove_inner.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html>
+<body>
+ <input id="target" type="text">
+ <script>
+ const target = document.getElementById('target');
+ target.addEventListener('mousemove', () => {
+ // 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>
Original Bug Report
Heap-use-after-free in ClickDispatcher constructor during Actor frame teardown
Report description
Heap-use-after-free in ClickDispatcher constructor during Actor frame teardown
Bug location
Where do you want to report your vulnerability?
Chrome VRP – Report security issues affecting the Chrome browser. See program rules
Which URL (or repository) have you found the vulnerability in?
Chrome 151.0.7922.0, official ASan build from the chromium-browser-asan GCS bucket, BuildId 5c8f91628a04675e (refs/branch-heads/7922); Linux x86_64. Crash files unchanged as of main 5f51b75ab67ce.
The problem
Please describe the technical details of the vulnerability
Title:* Heap-use-after-free in ClickDispatcher constructor during Actor frame teardownEnvironment: Chrome 151.0.7922.0, official ASan build from the chromium-browser-asan GCS bucket, BuildId 5c8f91628a04675e (refs/branch-heads/7922); Linux x86_64. Crash files unchanged as of main 5f51b75ab67ce.
Minimal reproduction:
cd poc && ./run.sh [path-to-asan-chrome]— startsserver.py(python3, 127.0.0.1/127.0.0.2:8000) and launches Chrome with the self-driving extension (--load-extension,--enable-features=AllowlistedActionsExtensions:allowlisted_extension_ids/<poc-id>,GlicActorSkipScreenshot --enable-extension-actor-api --headless=new).- The extension creates an Actor task, opens the victim page (outer → mid OOPIF → inner iframe), requests tab observation, and sends one Click action on the inner-frame button.
- Success = renderer ASan
heap-use-after-free~5–8 s after launch;run.shwrites the complete symbolized report (asan_report_symbolized.txt, attached).
Root cause: ClickTool::Execute constructs its ClickDispatcher inline inside the tool allocation (click_dispatcher_.emplace, click_tool.cc:150). With kGlicActorMoveBeforeClick (default-enabled) the constructor synchronously dispatches a synthetic mousemove into the page before returning (click_dispatcher.cc:61). The page’s handler removes the tool’s owning iframe; frame teardown runs on-stack and frees the 400-byte ClickTool allocation containing the under-construction dispatcher. The constructor resumes; its WeakPtrFactory is never destructed (the std::optional was never engaged), so the weak guard passes, and the constructor’s subsequent GetWeakPtr() (click_dispatcher.cc:72) reads freed memory.
ASan excerpt:
ERROR: AddressSanitizer: heap-use-after-free
READ of size 8 at 0x7678f547e370 (304 bytes inside of 400-byte region) thread T0 (chrome)
#0 base::internal::WeakReferenceOwner::GetRef() base/memory/scoped_refptr.h:260:62
#1 actor::ClickDispatcher::ClickDispatcher(...) base/memory/weak_ptr.h:382:45
#2 actor::ClickTool::Execute(...) third_party/libc++/construct_at.h:37:49
#3 actor::ToolExecutor::ExecuteTool(...) chrome/renderer/actor/tool_executor.cc:215:10
#4 ChromeRenderFrameObserver::ExecuteTool(...) chrome/renderer/chrome_render_frame_observer.cc:654:19
freed by:
operator delete
actor::ToolExecutor::~ToolExecutor()
ChromeRenderFrameObserver::~ChromeRenderFrameObserver()
content::RenderFrameImpl::FrameDetached(...) content/renderer/render_frame_impl.cc:3901:3
blink::Frame::Detach(...) third_party/blink/.../frame.cc:172:12
blink::HTMLFrameOwnerElement::DisconnectContentFrame() third_party/blink/.../html_frame_owner_element.cc:380:19
allocated by:
operator new
std::make_unique<actor::ClickTool>(...) third_party/libc++/unique_ptr.h:756:26
actor::ToolExecutor::InitializeToolImpl(...) chrome/renderer/actor/tool_executor.cc:93:15
MiraclePtr Status: NOT PROTECTED ("This crash is still exploitable with MiraclePtr.")
The same pattern appears reachable through TypeTool::Execute (type_tool.cc:486); details available on request.
Impact analysis
Impact: A webpage being acted on by a Glic Actor task can synchronously detach its own frame during the synthetic mousemove dispatched by the ClickDispatcher constructor, causing a deterministic renderer-process heap-use-after-free in Chrome’s normal ASan build.
Attacker position and reachability: The attacker is the page being acted on: it controls its own event handlers, and its mousemove handler removes the frame the Actor is clicking into. The ordinary user action is asking Glic (Gemini in Chrome) to perform a click task on that page. No non-default capability is required of the attacker: the features on the crash path (kGlicActorMoveBeforeClick, kGlicActorSplitValidateAndExecute, kGlicActorUiMagicCursor) are default-enabled and not overridden by the PoC. The extension API and allowlist flag are only a test driver — production Glic Actor tasks reach the identical renderer-side InitializeTool/ExecuteTool path and the same ClickTool::Execute code.
The cause
What version of Chrome have you found the security issue in?
Chrome 151.0.7922.0, official ASan build from the chromium-browser-asan GCS bucket, BuildId 5c8f91628a04675e (refs/branch-heads/7922); Linux x86_64. Crash files unchanged as of main 5f51b75ab67ce.
Is the security issue related to a crash?
Yes, it is related to a crash.
Choose the type of vulnerability
Memory Corruption (in a sandboxed process)
How would you like to be publicly acknowledged for your report?
Joshua Uphold