CVE-2026-19145
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
TEST_Fchrome/renderer/translate/translate_agent_browsertest.cc |
modified | |
ifcomponents/translate/content/renderer/translate_agent.cc |
modified |
Files Changed
chrome/renderer/translate/translate_agent_browsertest.cccomponents/translate/content/renderer/translate_agent.cc
Patch
From f5b53cdd00429721e94ace5cef61c7066253d0ed Mon Sep 17 00:00:00 2001
From: Andrew Paseltiner <apaseltiner@chromium.org>
Date: Tue, 21 Jul 2026 09:26:48 -0700
Subject: [PATCH] [translate] Handle frame detachment during script execution
TranslateAgent drives page translation by executing JavaScript in an
isolated world. Some of these scripts (the initial library injection,
cr.googleTranslate.translate(), and cr.googleTranslate.revert()) can
mutate the DOM and trigger main-world script that spins a nested run
loop. If that loop processes a frame-detach IPC,
TranslateAgent::OnDestruct() deletes |this| while
TranslateFrame()/TranslatePageImpl()/ CheckTranslateStatus() are still
on the stack, and the next virtual call goes through a stale pointer.
This change takes a WeakPtr from the (previously unused)
weak_pointer_factory_ before each script-executing call and bail out if
it has been invalidated, mirroring the pattern already used in
PrintRenderFrameHelper.
Fixed: 521878431
Change-Id: Ic35cc4086fa561c9f3723caa9a1b2d5cf9251903
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8120292
Reviewed-by: Danilo Tedeschi <danft@google.com>
Commit-Queue: Andrew Paseltiner <apaseltiner@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1665548}
---
diff --git a/chrome/renderer/translate/translate_agent_browsertest.cc b/chrome/renderer/translate/translate_agent_browsertest.cc
index 8e5dccb..030b66d 100644
--- a/chrome/renderer/translate/translate_agent_browsertest.cc
+++ b/chrome/renderer/translate/translate_agent_browsertest.cc
@@ -13,6 +13,7 @@
#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/path_service.h"
#include "base/run_loop.h"
#include "base/time/time.h"
@@ -598,6 +599,59 @@
EXPECT_TRUE(fake_translate_driver_.details_->adopted_language.empty());
}
+// Tests that the agent gracefully handles being deleted while it is injecting
+// the translate library script (e.g. when script execution spins a nested run
+// loop that tears down the owning frame).
+TEST_F(TranslateAgentBrowserTest, AgentDeletedDuringScriptInjection) {
+ // Take raw ownership so the agent can be deleted from inside a mock action.
+ TestTranslateAgent* agent = translate_agent_.release();
+
+ EXPECT_CALL(*agent, IsTranslateLibAvailable()).WillOnce(Return(false));
+ EXPECT_CALL(*agent, ExecuteScript(_)).WillOnce([agent](const std::string&) {
+ delete agent;
+ });
+
+ agent->TranslateFrame(std::string(), "en", "fr", base::DoNothing());
+}
+
+// Tests that the agent gracefully handles being deleted while it is starting
+// the translation.
+TEST_F(TranslateAgentBrowserTest, AgentDeletedDuringStartTranslation) {
+ TestTranslateAgent* agent = translate_agent_.release();
+
+ EXPECT_CALL(*agent, IsTranslateLibAvailable()).WillOnce(Return(true));
+ EXPECT_CALL(*agent, IsTranslateLibReady()).WillOnce(Return(true));
+ EXPECT_CALL(*agent, ExecuteScriptAndGetDoubleResult(_))
+ .WillRepeatedly(Return(0.0));
+ EXPECT_CALL(*agent, StartTranslation()).WillOnce([agent]() {
+ delete agent;
+ return false;
+ });
+
+ agent->TranslateFrame(std::string(), "en", "fr", base::DoNothing());
+}
+
+// Tests that the agent gracefully handles being deleted while it is checking
+// the translation status.
+TEST_F(TranslateAgentBrowserTest, AgentDeletedDuringCheckTranslateStatus) {
+ TestTranslateAgent* agent = translate_agent_.release();
+
+ base::RunLoop run_loop;
+ EXPECT_CALL(*agent, IsTranslateLibAvailable()).WillOnce(Return(true));
+ EXPECT_CALL(*agent, IsTranslateLibReady()).WillOnce(Return(true));
+ EXPECT_CALL(*agent, ExecuteScriptAndGetDoubleResult(_))
+ .WillRepeatedly(Return(0.0));
+ EXPECT_CALL(*agent, StartTranslation()).WillOnce(Return(true));
+ EXPECT_CALL(*agent, HasTranslationFailed()).WillOnce([agent, &run_loop]() {
+ delete agent;
+ run_loop.Quit();
+ return false;
+ });
+
+ agent->TranslateFrame(std::string(), "en", "fr", base::DoNothing());
+ run_loop.Run();
+}
+
#if BUILDFLAG(ENABLE_PDF)
TEST_F(TranslateAgentBrowserTest, PdfPageCaptured) {
GURL url("https://example.com");
diff --git a/components/translate/content/renderer/translate_agent.cc b/components/translate/content/renderer/translate_agent.cc
index f8900ef..24db02f 100644
--- a/components/translate/content/renderer/translate_agent.cc
+++ b/components/translate/content/renderer/translate_agent.cc
@@ -509,21 +509,39 @@
// Set up v8 isolated world.
EnsureIsolatedWorldInitialized(world_id_);
+ // Executing script may spin a nested run loop that detaches the frame and
+ // deletes |this|.
+ auto weak_this = weak_pointer_factory_.GetWeakPtr();
if (!IsTranslateLibAvailable()) {
+ if (!weak_this) {
+ return;
+ }
// Evaluate the script to add the translation related method to the global
// context of the page.
ExecuteScript(translate_script);
+ if (!weak_this) {
+ return;
+ }
DCHECK(IsTranslateLibAvailable());
}
+ if (!weak_this) {
+ return;
+ }
TranslatePageImpl(0);
}
void TranslateAgent::RevertTranslation() {
+ // Executing script may spin a nested run loop that detaches the frame and
+ // deletes |this|.
+ auto weak_this = weak_pointer_factory_.GetWeakPtr();
if (!IsTranslateLibAvailable()) {
DUMP_WILL_BE_NOTREACHED();
return;
}
+ if (!weak_this) {
+ return;
+ }
CancelPendingTranslation();
@@ -533,19 +551,38 @@
////////////////////////////////////////////////////////////////////////////////
// TranslateAgent, private:
void TranslateAgent::CheckTranslateStatus() {
+ // Executing script may spin a nested run loop that detaches the frame and
+ // deletes |this|.
+ auto weak_this = weak_pointer_factory_.GetWeakPtr();
// First check if there was an error.
if (HasTranslationFailed()) {
- NotifyBrowserTranslationFailed(
- static_cast<translate::TranslateErrors>(GetErrorCode()));
+ if (!weak_this) {
+ return;
+ }
+ TranslateErrors error =
+ static_cast<translate::TranslateErrors>(GetErrorCode());
+ if (!weak_this) {
+ return;
+ }
+ NotifyBrowserTranslationFailed(error);
return; // There was an error.
}
+ if (!weak_this) {
+ return;
+ }
if (HasTranslationFinished()) {
+ if (!weak_this) {
+ return;
+ }
std::string actual_source_lang;
// Translation was successfull, if it was auto, retrieve the source
// language the Translate Element detected.
if (source_lang_ == kAutoDetectionLanguage) {
actual_source_lang = GetPageSourceLanguage();
+ if (!weak_this) {
+ return;
+ }
if (actual_source_lang.empty()) {
NotifyBrowserTranslationFailed(TranslateErrors::UNKNOWN_LANGUAGE);
return;
@@ -564,6 +601,9 @@
// Check JavaScript performance counters for UMA reports.
ReportTimeToTranslate(
ExecuteScriptAndGetDoubleResult("cr.googleTranslate.translationTime"));
+ if (!weak_this) {
+ return;
+ }
ReportTranslatedLanguageDetectionContentLength(page_contents_length_);
// Notify the browser we are done.
@@ -571,6 +611,9 @@
.Run(false, actual_source_lang, target_lang_, TranslateErrors::NONE);
return;
}
+ if (!weak_this) {
+ return;
Regression Test / PoC
diff --git a/chrome/renderer/translate/translate_agent_browsertest.cc b/chrome/renderer/translate/translate_agent_browsertest.cc
index 8e5dccb..030b66d 100644
--- a/chrome/renderer/translate/translate_agent_browsertest.cc
+++ b/chrome/renderer/translate/translate_agent_browsertest.cc
@@ -13,6 +13,7 @@
#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/path_service.h"
#include "base/run_loop.h"
#include "base/time/time.h"
@@ -598,6 +599,59 @@
EXPECT_TRUE(fake_translate_driver_.details_->adopted_language.empty());
}
+// Tests that the agent gracefully handles being deleted while it is injecting
+// the translate library script (e.g. when script execution spins a nested run
+// loop that tears down the owning frame).
+TEST_F(TranslateAgentBrowserTest, AgentDeletedDuringScriptInjection) {
+ // Take raw ownership so the agent can be deleted from inside a mock action.
+ TestTranslateAgent* agent = translate_agent_.release();
+
+ EXPECT_CALL(*agent, IsTranslateLibAvailable()).WillOnce(Return(false));
+ EXPECT_CALL(*agent, ExecuteScript(_)).WillOnce([agent](const std::string&) {
+ delete agent;
+ });
+
+ agent->TranslateFrame(std::string(), "en", "fr", base::DoNothing());
+}
+
+// Tests that the agent gracefully handles being deleted while it is starting
+// the translation.
+TEST_F(TranslateAgentBrowserTest, AgentDeletedDuringStartTranslation) {
+ TestTranslateAgent* agent = translate_agent_.release();
+
+ EXPECT_CALL(*agent, IsTranslateLibAvailable()).WillOnce(Return(true));
+ EXPECT_CALL(*agent, IsTranslateLibReady()).WillOnce(Return(true));
+ EXPECT_CALL(*agent, ExecuteScriptAndGetDoubleResult(_))
+ .WillRepeatedly(Return(0.0));
+ EXPECT_CALL(*agent, StartTranslation()).WillOnce([agent]() {
+ delete agent;
+ return false;
+ });
+
+ agent->TranslateFrame(std::string(), "en", "fr", base::DoNothing());
+}
+
+// Tests that the agent gracefully handles being deleted while it is checking
+// the translation status.
+TEST_F(TranslateAgentBrowserTest, AgentDeletedDuringCheckTranslateStatus) {
+ TestTranslateAgent* agent = translate_agent_.release();
+
+ base::RunLoop run_loop;
+ EXPECT_CALL(*agent, IsTranslateLibAvailable()).WillOnce(Return(true));
+ EXPECT_CALL(*agent, IsTranslateLibReady()).WillOnce(Return(true));
+ EXPECT_CALL(*agent, ExecuteScriptAndGetDoubleResult(_))
+ .WillRepeatedly(Return(0.0));
+ EXPECT_CALL(*agent, StartTranslation()).WillOnce(Return(true));
+ EXPECT_CALL(*agent, HasTranslationFailed()).WillOnce([agent, &run_loop]() {
+ delete agent;
+ run_loop.Quit();
+ return false;
+ });
+
+ agent->TranslateFrame(std::string(), "en", "fr", base::DoNothing());
+ run_loop.Run();
+}
+
#if BUILDFLAG(ENABLE_PDF)
TEST_F(TranslateAgentBrowserTest, PdfPageCaptured) {
GURL url("https://example.com");
Original Bug Report
Use-After-Free in TranslateAgent via nested message loop
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 potential Use-After-Free in TranslateAgent can be triggered when synchronous JavaScript execution allows a nested message loop to process frame detachment IPCs. This results in the destruction of TranslateAgent while its methods are still on the call stack. The vulnerability bypasses MiraclePtr because all tracking raw_ptrs are destroyed before the memory is accessed, leading to potential Remote Code Execution in the renderer.
Affected files:
components/translate/content/renderer/translate_agent.cc
Estimated timestamp from git blame: Unknown (Google3 checkout)
Description
A potential Use-After-Free (UAF) vulnerability exists in TranslateAgent within the Chromium renderer process. TranslateAgent is a content::RenderFrameObserver that manages page translation. It initiates translation by calling synchronous JavaScript execution APIs (e.g., ExecuteScriptAndGetBoolResult).
When TranslateAgent::TranslatePageImpl calls StartTranslation(), it synchronously executes the Google Translate JavaScript. This script modifies the DOM (e.g., appending stylesheets or altering text nodes). In Blink, DOM modifications synchronously flush the custom element reaction stack. If an attacker controls a custom element affected by these modifications, their connectedCallback is executed synchronously while the ExecuteScript call is still on the stack.
Inside this callback, an attacker can trigger a synchronous Mojo IPC, such as alert() or confirm(). These map to [Sync] mojom calls (e.g., RunModalAlertDialog), which spin up a nested message loop to wait for the browser process’s reply. Crucially, this nested loop continues to process incoming IPC messages on the same associated interface group to prevent deadlocks.
If the attacker simultaneously triggers the frame to close from another window, the browser process sends a mojom::Frame::Delete IPC to the renderer. The nested message loop processes this IPC, invoking RenderFrameImpl::FrameDetached, which initiates frame teardown and calls delete this on the RenderFrameImpl.
During destruction, RenderFrameImpl iterates through its observers. ChromeRenderFrameObserver is destroyed first, destroying its raw_ptr<TranslateAgent>. Then, TranslateAgent::OnDestruct() is called, which calls delete this and removes itself from the RenderFrameImpl’s ObserverList (which also uses raw_ptr internally).
Because all raw_ptr references to the TranslateAgent are destroyed, MiraclePtr’s (BackupRefPtr) reference count drops to 0. The memory is fully released to PartitionAlloc without entering quarantine. The attacker can then reclaim this memory using JavaScript ArrayBuffer allocations.
When the alert() dialog is dismissed, the nested loop unwinds, and ExecuteScriptAndGetBoolResult returns false. Execution resumes in TranslatePageImpl on the freed, attacker-controlled this pointer:
if (!StartTranslation()) {
CheckTranslateStatus(); // UAF on 'this'
return;
}
CheckTranslateStatus() calls HasTranslationFailed(), which executes ExecuteScriptAndGetBoolResult. This method attempts to access render_frame_ (now controlled by the attacker) and calls the pure virtual method GetWebFrame() on it. This results in a deterministic vtable hijack and potential Remote Code Execution (RCE).
Suggested Steps to Reproduce (Potential)
Note: Our tooling agent does not yet have the ability to run code, so these are theoretical steps based on source code analysis.
- Host a malicious web page containing an iframe.
- Inside the iframe, register a Custom Element (e.g.,
<attack-element>) with aconnectedCallback. - Set the iframe’s language to trigger automatic translation.
- When translation begins, the Google Translate script will alter the DOM, synchronously triggering the
<attack-element>’sconnectedCallback. - Inside the callback, call
alert()to spin a nested message loop. - From the parent window (or a Web Worker), navigate or close the iframe, prompting the browser to send a frame deletion IPC.
- The renderer’s nested loop processes the deletion IPC, destroying the frame and the
TranslateAgent, fully freeing its memory. - From the parent window/Worker, rapidly allocate ArrayBuffers of the same size as
TranslateAgentto reclaim the memory and fake a vtable. - Dismiss the
alert()dialog. The execution unwinds, hits the UAF, and dereferences the fake vtable, executing attacker code.
Suggested Fix
TranslateAgent lacks survival checks after returning from synchronous JavaScript execution. The class should utilize base::WeakPtr to check if it has been destroyed during script execution.
For example, in TranslatePageImpl:
void TranslateAgent::TranslatePageImpl(int count) {
base::WeakPtr<TranslateAgent> weak_this = weak_method_factory_.GetWeakPtr();
// ...
if (!StartTranslation()) {
if (!weak_this) return; // Check if destroyed during script execution
CheckTranslateStatus();
return;
}
// ...
}
Similar checks should be applied after any call to ExecuteScript or its variants.
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.