Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in IME
DescriptionUse after free in IME
ComponentIME
Bug ClassUAF
Tracker506149253
Fix commit3474a9978fb6 (chromium/src) +20/-3
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-30

Changed Functions

FunctionChangeNotes
if
third_party/blink/renderer/platform/widget/widget_base.cc
modified

Files Changed

  • third_party/blink/renderer/platform/widget/input/ime_event_guard.cc
  • third_party/blink/renderer/platform/widget/input/ime_event_guard.h
  • third_party/blink/renderer/platform/widget/widget_base.cc
From 3474a9978fb63e39e5f73ba35db253caa5f40e19 Mon Sep 17 00:00:00 2001
From: Dave Tapuska <dtapuska@chromium.org>
Date: Tue, 12 May 2026 13:03:34 -0700
Subject: [PATCH] Add validity checks after frame_widget calls in IME methods.

Introduce ImeEventGuard::IsValid() to check if the associated WidgetBase
is still alive. Use this check in WidgetBase's SetComposition,
CommitText, and FinishComposingText methods after calling the
corresponding frame_widget methods. This prevents use-after-free issues
if the frame_widget call causes the WidgetBase to be destroyed.

BUG=506149253

Change-Id: Ic17e5ee9e39334d8afe1429e1c755e57ed344bbc
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7840643
Commit-Queue: Dave Tapuska <dtapuska@chromium.org>
Reviewed-by: Jonathan Ross <jonross@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1629520}
---

diff --git a/third_party/blink/renderer/platform/widget/input/ime_event_guard.cc b/third_party/blink/renderer/platform/widget/input/ime_event_guard.cc
index 10bb4343..98645b16 100644
--- a/third_party/blink/renderer/platform/widget/input/ime_event_guard.cc
+++ b/third_party/blink/renderer/platform/widget/input/ime_event_guard.cc
@@ -17,6 +17,10 @@
   widget_->OnImeEventGuardStart(this);
 }
 
+bool ImeEventGuard::IsValid() const {
+  return !!widget_;
+}
+
 ImeEventGuard::~ImeEventGuard() {
   if (widget_)
     widget_->OnImeEventGuardFinish(this);
diff --git a/third_party/blink/renderer/platform/widget/input/ime_event_guard.h b/third_party/blink/renderer/platform/widget/input/ime_event_guard.h
index 042e9c7..1190857 100644
--- a/third_party/blink/renderer/platform/widget/input/ime_event_guard.h
+++ b/third_party/blink/renderer/platform/widget/input/ime_event_guard.h
@@ -20,6 +20,9 @@
   explicit ImeEventGuard(base::WeakPtr<WidgetBase> widget);
   ~ImeEventGuard();
 
+  // Returns true if the widget is still alive.
+  bool IsValid() const;
+
   bool show_virtual_keyboard() const { return show_virtual_keyboard_; }
   void set_show_virtual_keyboard(bool show) { show_virtual_keyboard_ = show; }
 
diff --git a/third_party/blink/renderer/platform/widget/widget_base.cc b/third_party/blink/renderer/platform/widget/widget_base.cc
index b03e6e1..54640ab 100644
--- a/third_party/blink/renderer/platform/widget/widget_base.cc
+++ b/third_party/blink/renderer/platform/widget/widget_base.cc
@@ -1509,9 +1509,13 @@
   }
 
   ImeEventGuard guard(weak_ptr_factory_.GetWeakPtr());
-  if (!frame_widget->SetComposition(text, ime_text_spans, replacement_range,
-                                    selection_start, selection_end,
-                                    ime_state)) {
+  bool success =
+      frame_widget->SetComposition(text, ime_text_spans, replacement_range,
+                                   selection_start, selection_end, ime_state);
+  if (!guard.IsValid()) {
+    return;
+  }
+  if (!success) {
     // If we failed to set the composition text, then we need to let the browser
     // process to cancel the input method's ongoing composition session, to make
     // sure we are in a consistent state.
@@ -1543,6 +1547,9 @@
   input_handler_.set_handling_input_event(true);
   frame_widget->CommitText(text, ime_text_spans, replacement_range,
                            relative_cursor_pos);
+  if (!guard.IsValid()) {
+    return;
+  }
   input_handler_.set_handling_input_event(false);
   UpdateCompositionInfo(false /* not an immediate request */);
 }
@@ -1562,6 +1569,9 @@
   ImeEventGuard guard(weak_ptr_factory_.GetWeakPtr());
   input_handler_.set_handling_input_event(true);
   frame_widget->FinishComposingText(keep_selection);
+  if (!guard.IsValid()) {
+    return;
+  }
   input_handler_.set_handling_input_event(false);
   UpdateCompositionInfo(false /* not an immediate request */);
 }
Loading diff…

Original Bug Report

reported by vm...@google.com

Use-After-Free in WidgetBase during IME event handling

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 potential Use-After-Free (UAF) vulnerability exists in WidgetBase::ImeCommitText and related IME methods. Synchronous JavaScript execution during text insertion can enter a nested message loop (e.g., via window.print()), allowing a pending frame detachment IPC to destroy the WidgetBase object before the stack unwinds, leading to a UAF and a virtual call on a freed object.

Affected files:

  • third_party/blink/renderer/platform/widget/widget_base.cc
  • third_party/blink/renderer/platform/widget/widget_base.h

Estimated timestamp from git blame: 2026-01-08

Summary

A potential Use-After-Free (UAF) vulnerability has been identified in the WidgetBase class within the Blink renderer. The issue arises when IME-related functions (ImeCommitText, ImeSetComposition, and ImeFinishComposingText) dispatch synchronous JavaScript events and subsequently access member variables without verifying if the WidgetBase instance is still alive.

Root Cause

In third_party/blink/renderer/platform/widget/widget_base.cc, functions like WidgetBase::ImeCommitText call frame_widget->CommitText(...) to process text input. This call traverses down to InputMethodController::CommitText, which uses an EventQueueScope to buffer DOM events during the text insertion.

When the EventQueueScope is destroyed, it synchronously flushes the queued events, such as compositionend. If JavaScript attached to this event calls window.print(), the renderer enters a nested run loop (via PrintRenderFrameHelper::RequestPrintPreview).

While spinning this nested run loop, the renderer can process pending IPC messages. Specifically, a mojom::Frame::Delete IPC (triggered if a parent frame concurrently removes the iframe) is processed because it is bound to the TaskType::kInternalNavigationAssociated task runner. Tasks on this queue have FreezableTaskQueueTraits, meaning they are not paused by the ScopedPagePauser that window.print() initializes.

Processing the Delete IPC results in WebFrameWidgetImpl::Close, which calls widget_base_.reset(), synchronously destroying the WidgetBase object.

When the print dialog is closed and the stack unwinds, execution returns to WidgetBase::ImeCommitText:

// widget_base.cc:1545
  frame_widget->CommitText(text, ime_text_spans, replacement_range,
                           relative_cursor_pos);
  input_handler_.set_handling_input_event(false); // UAF write
  UpdateCompositionInfo(false /* not an immediate request */); // UAF read & virtual call

The implicit this pointer on the stack is now dangling. The subsequent call to UpdateCompositionInfo performs a read on client_ and then a virtual call (client_->FrameWidget()).

Crucially, because the dangling pointer is held as an implicit this pointer on the stack rather than a raw_ptr<T> class member, this UAF is not protected by MiraclePtr.

Potential Impact

This vulnerability provides a reliable path to Remote Code Execution (RCE) within the sandboxed renderer process. By spraying the heap while the nested loop is spinning, an attacker can reallocate the memory freed by WidgetBase and place a fake vtable at the location of client_, hijacking the subsequent virtual call.

Suggested Reproduction Steps

Note: These steps are suggested as we do not execute code within this environment.

  1. Host an attacker-controlled page that embeds a cross-site (or same-origin) child iframe.
  2. In the child iframe, register a compositionend event listener on an editable element. The listener should call window.print().
  3. The parent frame concurrently schedules the removal of the child iframe (e.g., via setTimeout(() => iframe.remove(), ...)).
  4. A user focuses the input field in the child iframe and types using an IME, triggering an IPC that reaches WidgetBase::ImeCommitText.
  5. The synchronous compositionend event fires, executing window.print(), which spins a nested run loop.
  6. The mojom::Frame::Delete IPC arrives from the browser process, is processed in the nested loop, and destroys the WidgetBase object.
  7. The print dialog is closed, the stack unwinds, and the C++ code performs a UAF read/write and a virtual method call on the freed WidgetBase object.

Proposed Fix

Similar to how WidgetBaseInputHandler::HandleInputEvent safely checks for widget destruction, IME functions should perform a liveness check after returning from synchronous dispatch points.

Either check the base::WeakPtr implicitly held by the ImeEventGuard (if exposed), or create a local base::WeakPtr<WidgetBase> before the call to frame_widget->CommitText(...) and return early if it is invalidated:

  base::WeakPtr<WidgetBase> weak_this = weak_ptr_factory_.GetWeakPtr();
  frame_widget->CommitText(text, ime_text_spans, replacement_range,
                           relative_cursor_pos);
  if (!weak_this)
    return;

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.

View on issue tracker