Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Translate
DescriptionUse after free in Translate
ComponentTranslate
Bug ClassUAF
Tracker521878431
Fix commitf5b53cdd0042 (chromium/src) +123/-2
CISA KEVNot listed
CreditedGoogle
Disclosed2026-08-06

Changed Functions

FunctionChangeNotes
TEST_F
chrome/renderer/translate/translate_agent_browsertest.cc
modified
if
components/translate/content/renderer/translate_agent.cc
modified

Files Changed

  • chrome/renderer/translate/translate_agent_browsertest.cc
  • components/translate/content/renderer/translate_agent.cc
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;
Loading diff…

Regression Test / PoC

shipped with the fix
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");
Loading diff…

Original Bug Report

reported by rj...@google.com

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.

  1. Host a malicious web page containing an iframe.
  2. Inside the iframe, register a Custom Element (e.g., <attack-element>) with a connectedCallback.
  3. Set the iframe’s language to trigger automatic translation.
  4. When translation begins, the Google Translate script will alter the DOM, synchronously triggering the <attack-element>’s connectedCallback.
  5. Inside the callback, call alert() to spin a nested message loop.
  6. From the parent window (or a Web Worker), navigate or close the iframe, prompting the browser to send a frame deletion IPC.
  7. The renderer’s nested loop processes the deletion IPC, destroying the frame and the TranslateAgent, fully freeing its memory.
  8. From the parent window/Worker, rapidly allocate ArrayBuffers of the same size as TranslateAgent to reclaim the memory and fake a vtable.
  9. 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.

View on issue tracker