Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Core
DescriptionUse after free in Core
ComponentCore
Bug ClassUAF
Tracker523609602
Fix commit7b5b6d15720c (chromium/src) +94/-10
CISA KEVNot listed
CreditedGoogle
Disclosed2026-07-08

Changed Functions

FunctionChangeNotes
if
content/browser/renderer_host/render_widget_host_view_aura.cc
modified
if
content/browser/renderer_host/render_widget_host_view_aura_unittest.cc
modified
RenderWidgetHostViewAuraOnBoundsChangedUAFTest
content/browser/renderer_host/render_widget_host_view_aura_unittest.cc
modified

Files Changed

  • content/browser/renderer_host/render_widget_host_view_aura.cc
  • content/browser/renderer_host/render_widget_host_view_aura_unittest.cc
From 7b5b6d15720ccaebebe72ef16ed27c68457b1c63 Mon Sep 17 00:00:00 2001
From: kylechar <[email protected]>
Date: Fri, 26 Jun 2026 07:43:06 -0700
Subject: [PATCH] Guard against re-entrant RWHVA destruction

The Windows TSF IME can pump a nested message loop and potentially cause
RenderWidgetHostViewAura destruction. This targets call sites where
RWHVA calls into InputMethod[WinTFS] / TSFBridgeImpl that were
identified as potentially running a nested message loop. If there was
access to the RWHV object after nested message loop a WeakPtr guard is
added.

Additionally RWHVA::OnUpdateTextInputStateCalled() calls
TextInputManager::NotifySelectionBoundsChanged() which calls back into
RenderWidgetHostViewAura::OnSelectionBoundsChanged() and then
InputMethodWinTSF::OnCaretBoundsChanged(). This also requires a guard.

This extends on the approach from https://crrev.com/c/7819560.

Bug: 523609602
Change-Id: I0f345cca92861d19ac65a5795abe222a4c04482e
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7986587
Commit-Queue: Kyle Charbonneau <[email protected]>
Reviewed-by: Aman Verma <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1653146}
---

diff --git a/content/browser/renderer_host/render_widget_host_view_aura.cc b/content/browser/renderer_host/render_widget_host_view_aura.cc
index dd954a6..fb30dd6 100644
--- a/content/browser/renderer_host/render_widget_host_view_aura.cc
+++ b/content/browser/renderer_host/render_widget_host_view_aura.cc
@@ -2314,8 +2314,8 @@
   if (GetInputMethod()) {
     auto weak_this = weak_ptr_factory_.GetWeakPtr();
     GetInputMethod()->OnCaretBoundsChanged(this);
-    // `this` may have been deleted inside the IME callout.
     if (!weak_this) {
+      // `this` may have been deleted inside the IME callout.
       return;
     }
     UpdateInsetsWithVirtualKeyboardEnabled();
@@ -2533,8 +2533,13 @@
   last_pointer_type_before_focus_ = last_pointer_type_;
 
   auto* input_method = GetInputMethod();
-  if (input_method)
+  if (input_method) {
+    auto weak_this = weak_ptr_factory_.GetWeakPtr();
     input_method->CancelComposition(this);
+    if (!weak_this) {
+      return;
+    }
+  }
   has_composition_text_ = false;
 
 #if BUILDFLAG(IS_WIN)
@@ -2756,7 +2761,11 @@
     if (input_method) {
       // Ask the system-wide IME to send all TextInputClient messages to |this|
       // object.
+      auto weak_this = weak_ptr_factory_.GetWeakPtr();
       input_method->SetFocusedTextInputClient(this);
+      if (!weak_this) {
+        return;
+      }
     }
 
     ui::BrowserAccessibilityManager* manager =
@@ -3245,6 +3254,9 @@
   window_->GetHost()->AddObserver(this);
   UpdateScreenInfo();
 
+  base::WeakPtr<RenderWidgetHostViewAura> weak_this(
+      weak_ptr_factory_.GetWeakPtr());
+
   aura::client::CursorClient* cursor_client =
       aura::client::GetCursorClient(window_->GetRootWindow());
   if (cursor_client) {
@@ -3253,15 +3265,17 @@
   }
   if (HasFocus()) {
     ui::InputMethod* input_method = GetInputMethod();
-    if (input_method)
+    if (input_method) {
       input_method->SetFocusedTextInputClient(this);
+      if (!weak_this) {
+        return;
+      }
+    }
   }
 
 #if BUILDFLAG(IS_WIN)
   // `UpdateLegacyWin()` can spin a nested message loop on Windows, potentially
   // destroying `this`.
-  base::WeakPtr<RenderWidgetHostViewAura> weak_this(
-      weak_ptr_factory_.GetWeakPtr());
   UpdateLegacyWin();
   if (!weak_this) {
     return;
@@ -3297,7 +3311,11 @@
 void RenderWidgetHostViewAura::DetachFromInputMethod(bool is_removed) {
   ui::InputMethod* input_method = GetInputMethod();
   if (input_method) {
+    auto weak_this = weak_ptr_factory_.GetWeakPtr();
     input_method->DetachTextInputClient(this);
+    if (!weak_this) {
+      return;
+    }
 #if BUILDFLAG(IS_CHROMEOS)
     if (!window_->is_destroying()) {
       wm::RestoreWindowBoundsOnClientFocusLost(window_->GetToplevelWindow());
@@ -3420,8 +3438,15 @@
   if (!GetInputMethod())
     return;
 
-  if (did_update_state)
+  auto weak_this = weak_ptr_factory_.GetWeakPtr();
+
+  if (did_update_state) {
     GetInputMethod()->OnTextInputTypeChanged(this);
+    if (!weak_this) {
+      // `this` may have been deleted inside the IME callout.
+      return;
+    }
+  }
 
   const ui::mojom::TextInputState* state =
       text_input_manager_->GetTextInputState();
@@ -3463,6 +3488,10 @@
   // Ensure that selection bounds changes are sent to the IME.
   if (state && state->type != ui::TEXT_INPUT_TYPE_NONE) {
     text_input_manager->NotifySelectionBoundsChanged(updated_view);
+    if (!weak_this) {
+      // `this` may have been deleted inside the IME callout.
+      return;
+    }
   }
 
   if (auto* render_widget_host = updated_view->host()) {
@@ -3481,8 +3510,14 @@
   // TextInputManager::GetActiveWidget() as RenderWidgetHostViewAura can call
   // this method to finish any ongoing composition in response to a mouse down
   // event.
-  if (GetInputMethod())
+  if (GetInputMethod()) {
+    auto weak_this = weak_ptr_factory_.GetWeakPtr();
     GetInputMethod()->CancelComposition(this);
+    // `this` may have been deleted inside the IME callout.
+    if (!weak_this) {
+      return;
+    }
+  }
   has_composition_text_ = false;
 }
 
@@ -3516,8 +3551,14 @@
   // changed. e.g. When the rendered text is wider than the input field,
   // deleting the last character won't change the caret bounds but will change
   // the surrounding text.
-  if (GetInputMethod())
+  if (GetInputMethod()) {
+    auto weak_this = weak_ptr_factory_.GetWeakPtr();
     GetInputMethod()->OnCaretBoundsChanged(this);
+    if (!weak_this) {
+      // `this` may have been deleted inside the IME callout.
+      return;
+    }
+  }
 
   if (ui::Clipboard::IsSupportedClipboardBuffer(
           ui::ClipboardBuffer::kSelection)) {
diff --git a/content/browser/renderer_host/render_widget_host_view_aura_unittest.cc b/content/browser/renderer_host/render_widget_host_view_aura_unittest.cc
index 2c795c66..6d533d6 100644
--- a/content/browser/renderer_host/render_widget_host_view_aura_unittest.cc
+++ b/content/browser/renderer_host/render_widget_host_view_aura_unittest.cc
@@ -6929,15 +6929,27 @@
     }
   }
 
+  void OnTextInputTypeChanged(ui::TextInputClient* client) override {
+    ui::MockInputMethod::OnTextInputTypeChanged(client);
+    if (on_text_input_type_changed_) {
+      std::move(on_text_input_type_changed_).Run();
+    }
+  }
+
   void set_on_caret_bounds_changed(base::OnceClosure closure) {
     on_caret_bounds_changed_ = std::move(closure);
   }
 
+  void set_on_text_input_type_changed(base::OnceClosure closure) {
+    on_text_input_type_changed_ = std::move(closure);
+  }
+
  private:
   base::OnceClosure on_caret_bounds_changed_;
+  base::OnceClosure on_text_input_type_changed_;
 };
 
-class RenderWidgetHostViewAuraOnBoundsChangedUAFTest
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/content/browser/renderer_host/render_widget_host_view_aura_unittest.cc b/content/browser/renderer_host/render_widget_host_view_aura_unittest.cc
index 2c795c66..6d533d6 100644
--- a/content/browser/renderer_host/render_widget_host_view_aura_unittest.cc
+++ b/content/browser/renderer_host/render_widget_host_view_aura_unittest.cc
@@ -6929,15 +6929,27 @@
     }
   }
 
+  void OnTextInputTypeChanged(ui::TextInputClient* client) override {
+    ui::MockInputMethod::OnTextInputTypeChanged(client);
+    if (on_text_input_type_changed_) {
+      std::move(on_text_input_type_changed_).Run();
+    }
+  }
+
   void set_on_caret_bounds_changed(base::OnceClosure closure) {
     on_caret_bounds_changed_ = std::move(closure);
   }
 
+  void set_on_text_input_type_changed(base::OnceClosure closure) {
+    on_text_input_type_changed_ = std::move(closure);
+  }
+
  private:
   base::OnceClosure on_caret_bounds_changed_;
+  base::OnceClosure on_text_input_type_changed_;
 };
 
-class RenderWidgetHostViewAuraOnBoundsChangedUAFTest
+class RenderWidgetHostViewAuraReentrantDestructionIME
     : public RenderWidgetHostViewAuraTest {
  public:
   void SetUp() override {
@@ -6966,7 +6978,7 @@
 // ~AutoReset both touch freed memory. AutoReset::scoped_variable_ is
 // RAW_PTR_EXCLUSION, so it is not MiraclePtr-protected: the ~AutoReset write
 // lands in a freed (un-quarantined) slot.
-TEST_F(RenderWidgetHostViewAuraOnBoundsChangedUAFTest,
+TEST_F(RenderWidgetHostViewAuraReentrantDestructionIME,
        DestroyDuringOnCaretBoundsChanged) {
   InitViewForFrame(nullptr);
   ParentHostView(view_, parent_view_);
@@ -6991,6 +7003,37 @@
   raw_view->OnBoundsChanged(gfx::Rect(), gfx::Rect(0, 0, 100, 100));
 }
 
+// RWHVA::OnUpdateTextInputStateCalled() calls
+// GetInputMethod()->OnTextInputTypeChanged(this), which on Windows reaches
+// TSFBridge::OnTextInputTypeChanged() -> ITfThreadMgr::SetFocus(). If the
+// active TIP pumps the message queue and the view is destroyed re-entrantly,
+// on unwind the function continues to dereference the freed `this` and
+// `updated_view`.
+TEST_F(RenderWidgetHostViewAuraReentrantDestructionIME,
+       DestroyDuringOnTextInputTypeChanged) {
+  InitViewForFrame(nullptr);
+  ParentHostView(view_, parent_view_);
+  // `view_` shares the root window (and thus the InputMethod) with
+  // `parent_view_`.
+  ASSERT_EQ(static_cast<ui::InputMethod*>(input_method_.get()),
+            GetInputMethod());
+
+  // Arrange for the view to be synchronously destroyed inside
+  // OnTextInputTypeChanged, simulating re-entrant destruction triggered by a
+  // TSF callout that pumps a queued window.close() / renderer-gone task.
+  input_method_->set_on_text_input_type_changed(
+      base::BindLambdaForTesting([&]() {
+        widget_host_ = nullptr;
+        view_.ExtractAsDangling()->Destroy();
+      }));
+
+  ui::mojom::TextInputState state;
+  state.type = ui::TEXT_INPUT_TYPE_TEXT;
+  // Dispatching this state notifies `view_` (a TextInputManager observer) via
+  // OnUpdateTextInputStateCalled(), which calls into the input method above.
+  GetTextInputManager(view_)->UpdateTextInputState(view_, state);
+}
+
 #if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_CHROMEOS)
 class MockVirtualKeyboardController final
     : public ui::VirtualKeyboardController {
Loading diff…

Original Bug Report

reported by [email protected]

Potential Use-After-Free in RenderWidgetHostViewAura via synchronous IME calls

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. 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 exists in RenderWidgetHostViewAura when updating text input state due to unguarded synchronous IME subsystem calls. On Windows, these calls can trigger nested message loops that process pending destruction events, fully freeing the view object. Continuing execution using freed raw stack pointers bypasses MiraclePtr, potentially allowing a compromised renderer to achieve browser process RCE.

Affected files:

  • content/browser/renderer_host/render_widget_host_view_aura.cc

Estimated timestamp from git blame: Unknown (Google3 checkout)

Vulnerability Details

A potential Use-After-Free (UAF) vulnerability exists in content/browser/renderer_host/render_widget_host_view_aura.cc within the RenderWidgetHostViewAura::OnUpdateTextInputStateCalled method. This method performs synchronous calls into the Input Method Editor (IME) subsystem without guarding against the synchronous destruction of the this object.

Specifically, at line 3431, the method executes:

GetInputMethod()->OnTextInputTypeChanged(this);

On Windows, this routes to TSF (Text Services Framework) COM APIs (e.g., in InputMethodWinTSF::OnTextInputTypeChanged). These TSF COM calls can synchronously pump the UI thread’s message loop (such as via ::NotifyWinEvent). If a pending UI message triggers the destruction of the WebContents or window (e.g., a concurrent tab closure), RenderWidgetHostViewAura::~RenderWidgetHostViewAura will run synchronously on the same thread.

During destruction, the view unregisters itself from TextInputManager’s observer list. The ObserverList uses UncheckedObserverAdapter, which internally holds a raw_ptr. Calling RemoveObserver zeroes out this raw_ptr. As this was the final raw_ptr holding the object alive, the MiraclePtr (BackupRefPtr / BRP) reference count drops to zero, and PartitionAlloc fully frees the object’s memory.

However, the OnUpdateTextInputStateCalled function is executing with this and updated_view as raw C++ stack pointers (which do not increment BRP refcounts). When the TSF message loop unwinds and the IME call returns, execution continues on these freed pointers.

At line 3475, the code accesses the freed memory:

if (auto* render_widget_host = updated_view->host()) {
  render_widget_host->RequestCompositionUpdates(...);
}

If an attacker reallocates this memory, they can control the returned render_widget_host pointer. The call to RequestCompositionUpdates internally makes a virtual method call to GetWidgetInputHandler(), allowing an attacker to hijack control flow.

Potential Attacker Steps

Note: These are suggested/potential steps based on code analysis, as our tooling agent does not currently have the ability to run code or build a functional Proof of Concept.

  1. Gain Initial Execution: An attacker first compromises a renderer process (e.g., via a v8 vulnerability).
  2. Trigger State Update: The compromised renderer crafts and sends a mojom::WidgetHost::TextInputStateChanged IPC message to the browser process to update its text input state.
  3. Concurrent Destruction: Concurrently, the attacker triggers a window or tab closure event (e.g., via a secondary compromised renderer or Web Worker IPC).
  4. Message Loop Pumping: The TextInputStateChanged IPC reaches RenderWidgetHostViewAura::OnUpdateTextInputStateCalled, triggering the synchronous OnTextInputTypeChanged IME call. This pumps the message loop, processing the closure event and synchronously destroying the RenderWidgetHostViewAura object.
  5. Memory Reallocation: The attacker uses heap grooming/spraying in the browser process to immediately reallocate the freed RenderWidgetHostViewAura memory chunk with a fake object. This fake object is crafted to ensure the host_ pointer points to another fake RenderWidgetHostImpl object under the attacker’s control.
  6. Control Flow Hijack: The IME call returns. The browser uses the raw updated_view pointer to read the attacker-controlled host_ pointer and calls RequestCompositionUpdates. This triggers a virtual method call (GetWidgetInputHandler()) on the fake RenderWidgetHostImpl, jumping to an attacker-supplied address (vtable hijacking) and resulting in an unsandboxed browser process RCE.

Suggested Fix

Implement base::WeakPtr guards in RenderWidgetHostViewAura::OnUpdateTextInputStateCalled. Capture a WeakPtr to this before any IME subsystem calls, and explicitly check its validity before accessing any members or using the updated_view pointer afterwards.

base::WeakPtr<RenderWidgetHostViewAura> weak_this = weak_ptr_factory_.GetWeakPtr();

if (did_update_state)
  GetInputMethod()->OnTextInputTypeChanged(this);

if (!weak_this)
  return;
// ... proceed safely ...

Evaluated with Chrome root at commit: 65b3256311f3ab6fb9870eaa522de7e6dd2663bb


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