CVE-2026-15116
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchrome/renderer/actor/click_tool.cc |
modified | |
ifchrome/renderer/actor/script_tool.cc |
modified | |
ifchrome/renderer/actor/tool_executor.cc |
modified | |
ifchrome/renderer/actor/type_tool.cc |
modified |
Files Changed
chrome/renderer/actor/click_tool.ccchrome/renderer/actor/script_tool.ccchrome/renderer/actor/tool_executor.ccchrome/renderer/actor/type_tool.cc
Patch
From 4d43b652de342e3a6cb91f8c1c4b66e78caee057 Mon Sep 17 00:00:00 2001
From: Johann Hofmann <johannhof@chromium.org>
Date: Mon, 22 Jun 2026 14:08:23 -0700
Subject: [PATCH] Fix Use-After-Free in actor::ToolExecutor and ClickTool during Cancel
When cancelling a tool, the cancellation sequence can synchronously
dispatch events (like mouseup) which allow the page to detach the frame.
This destroys the RenderFrameImpl and associated objects, including
ToolExecutor and the active ClickTool, while they are still on the call
stack.
This CL adds WeakPtr checks after the cancellation calls to verify
if the objects are still alive before proceeding with unwinding/cleanup
code.
Bug: 522092013
TAG=agy
CONV=bcb8651f-d6f1-45e1-be85-def2fff71421
Change-Id: Ib46f14a44a381777ad24859c81f6053a18769b0a
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7958383
Reviewed-by: David Bokan <bokan@chromium.org>
Auto-Submit: Johann Hofmann <johannhof@chromium.org>
Commit-Queue: David Bokan <bokan@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1650573}
---
diff --git a/chrome/renderer/actor/click_tool.cc b/chrome/renderer/actor/click_tool.cc
index afc9a8b..de34f3f 100644
--- a/chrome/renderer/actor/click_tool.cc
+++ b/chrome/renderer/actor/click_tool.cc
@@ -110,8 +110,14 @@
void ClickTool::Cancel() {
if (click_dispatcher_) {
+ // click_dispatcher_->Cancel() synchronously dispatches DOM events that
+ // might destroy the owning frame and this tool. Use a weak pointer to
+ // detect if `this` is still valid.
+ base::WeakPtr<ClickTool> weak_this = weak_ptr_factory_.GetWeakPtr();
click_dispatcher_->Cancel();
- click_dispatcher_.reset();
+ if (weak_this) {
+ click_dispatcher_.reset();
+ }
}
}
diff --git a/chrome/renderer/actor/script_tool.cc b/chrome/renderer/actor/script_tool.cc
index a64c31a..8fd4322 100644
--- a/chrome/renderer/actor/script_tool.cc
+++ b/chrome/renderer/actor/script_tool.cc
@@ -112,8 +112,14 @@
if (!execution_id_.has_value()) {
return;
}
+ // CancelScriptTool() synchronously dispatches DOM events that might destroy
+ // the owning frame and this tool. Use a weak pointer to detect if `this` is
+ // still valid.
+ base::WeakPtr<ScriptTool> weak_this = weak_ptr_factory_.GetWeakPtr();
frame_->GetWebFrame()->GetDocument().CancelScriptTool(execution_id_.value());
- execution_id_.reset();
+ if (weak_this) {
+ execution_id_.reset();
+ }
}
std::string ScriptTool::DebugString() const {
diff --git a/chrome/renderer/actor/tool_executor.cc b/chrome/renderer/actor/tool_executor.cc
index 9230e74..34f58e50 100644
--- a/chrome/renderer/actor/tool_executor.cc
+++ b/chrome/renderer/actor/tool_executor.cc
@@ -270,11 +270,17 @@
// The browser and renderer should agree on the active tool.
CHECK_EQ(tool_->task_id(), task_id);
+ // tool_->Cancel() synchronously dispatches DOM events that might destroy the
+ // owning frame and this executor. Use a weak pointer to detect if `this` is
+ // still valid.
+ base::WeakPtr<ToolExecutor> weak_this = weak_ptr_factory_.GetWeakPtr();
tool_->Cancel();
- // The result code doesn't matter as it will be ignored by the browser
- // process.
- ToolFinished(MakeResult(mojom::ActionResultCode::kInvokeCanceled));
+ if (weak_this) {
+ // The result code doesn't matter as it will be ignored by the browser
+ // process.
+ ToolFinished(MakeResult(mojom::ActionResultCode::kInvokeCanceled));
+ }
}
void ToolExecutor::ToolFinished(mojom::ActionResultPtr result) {
diff --git a/chrome/renderer/actor/type_tool.cc b/chrome/renderer/actor/type_tool.cc
index c8aeb1db..c715ea0 100644
--- a/chrome/renderer/actor/type_tool.cc
+++ b/chrome/renderer/actor/type_tool.cc
@@ -501,13 +501,22 @@
// Clicking is completed before key dispatching, so there shouldn't be both.
CHECK(!(click_dispatcher_ && key_dispatcher_));
+ // click_dispatcher_->Cancel() or key_dispatcher_->Cancel() synchronously
+ // dispatches DOM events that might destroy the owning frame and this tool.
+ // Use a weak pointer to detect if `this` is still valid.
+ base::WeakPtr<TypeTool> weak_this = weak_ptr_factory_.GetWeakPtr();
+
if (click_dispatcher_) {
click_dispatcher_->Cancel();
- click_dispatcher_.reset();
+ if (weak_this) {
+ click_dispatcher_.reset();
+ }
}
if (key_dispatcher_) {
key_dispatcher_->Cancel();
- key_dispatcher_.reset();
+ if (weak_this) {
+ key_dispatcher_.reset();
+ }
}
}
Original Bug Report
Potential Use-After-Free in actor::ToolExecutor during synchronous frame detachment
Flapjack, 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: A Use-After-Free (UAF) vulnerability exists in the renderer process when cancelling an active Glic Actor tool. The cancellation sequence synchronously dispatches a DOM mouseup event, allowing a malicious page to detach the frame and destroy the active tool and its executor while they are still on the call stack. Upon return from the event dispatch, the unwinding code accesses the freed objects.
Affected files:
chrome/renderer/actor/click_tool.ccchrome/renderer/actor/tool_executor.cc
Estimated timestamp from git blame: 2025-12-10
Summary
A Use-After-Free (UAF) vulnerability has been identified in the actor::ClickTool and actor::ToolExecutor classes in the renderer process. The vulnerability occurs because the tool cancellation process (ToolExecutor::CancelTool) synchronously dispatches a mouseup event to the Blink engine via ClickDispatcher::DoMouseUpImpl. This synchronous dispatch allows an attacker-controlled JavaScript event listener to detach the frame, which synchronously destroys the RenderFrameImpl and its associated objects, including the ToolExecutor and the active ClickTool. When the synchronous event dispatch returns, the methods on the call stack continue execution on the freed memory.
Technical Details
To trigger this vulnerability, an attacker must win a race condition to ensure that the CancelTool IPC is processed before the 5ms delayed task in ClickDispatcher::DoMouseDown executes. This can be achieved reliably by stalling the renderer main thread.
Suggested steps an attacker might follow:
- Create a page with an
iframeand a target element inside it. - Add
mousedownandmouseupevent listeners to the target element. - Trigger the Actor feature to click the element. This causes
ToolExecutor::ExecuteToolto invokeClickTool::Execute, which creates aClickDispatcherthat synchronously fires themousedownevent. - In the
mousedownJavaScript listener, execute a busy-wait loop for 35 seconds to block the renderer main thread. - While the thread is blocked, the browser process’s 30-second
kGlicActorPageToolTimeoutexpires, causing it to send aCancelToolMojo IPC to the renderer. - When the 35-second busy-wait completes, the main thread resumes. The delayed 5ms
DoMouseUptask is not yet runnable, so the task scheduler processes the pendingCancelToolIPC. ChromeRenderFrameObserver::CancelToolcallstool_executor_->CancelTool(), which chains toClickTool::Cancel()and thenClickDispatcher::Cancel().- Since the 5ms timer hasn’t fired, the mouse is still “down”.
ClickDispatcher::Cancel()callsDoMouseUpImpl(), which synchronously dispatches themouseupevent viawidget->HandleInputEvent(). - The attacker’s
mouseuplistener is executed. The listener callswindow.frameElement.remove(), detaching the iframe. - Blink synchronously processes the detachment, calling
RenderFrameImpl::FrameDetached. This destroys theRenderFrameImpl, which notifiesChromeRenderFrameObserver::OnDestruct, destroying the observer. This implicitly destroys theToolExecutor,ClickTool, andClickDispatcher. - The attacker uses heap grooming in the
mouseuplistener to overwrite the freedToolExecutorchunk. - Execution unwinds back to
ClickTool::Cancel()atchrome/renderer/actor/click_tool.cc:114, which callsclick_dispatcher_.reset()on the freed object. - Execution unwinds to
ToolExecutor::CancelTool(), which callsToolFinished()(chrome/renderer/actor/tool_executor.cc:277). - Inside
ToolFinished()(chrome/renderer/actor/tool_executor.cc:280), the code accesses multiple fields on the freedthispointer and executesstd::move(completion_callback_).Run(), leading to potential Remote Code Execution (RCE) in the renderer process.
While ClickDispatcher::DoMouseUpImpl correctly checks !weak_this after returning from HandleInputEvent, its callers (ClickTool and ToolExecutor) do not verify if they were destroyed during the synchronous call.
(Note: These are suggested steps based on static analysis; our tooling does not yet execute code to provide a working PoC.)
Proposed Fix
The objects on the call stack must check for their own destruction after any synchronous call that can execute script. ClickTool::Cancel should use a base::WeakPtr to check its existence before accessing its members. Similarly, ToolExecutor::CancelTool must verify the ToolExecutor is still alive before calling ToolFinished or accessing this.
Evaluated with Chrome root at commit: 2155cb00003ec35716a76ed3246eae995f87b7ff
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.