High CVSS 8.3 chrome UAF 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
8.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in UI
DescriptionUse after free in UI
ComponentUI
Bug ClassUAF
Tracker502112506
Fix commita66bf3aeca0d (chromium/src) +91/-1
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:C/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-04-13 (per vendor advisory)
Fix landed2026-05-07 (public git)
Disclosed2026-05-27
Patch gap20 days — fix public in source before the advisory

Background

`base::AutoReset<bool>` vs `base::WeakAutoReset`
AutoReset stores a raw pointer to a member and writes a saved value back on destruction. Its scoped_variable_ pointer is RAW_PTR_EXCLUSION (not BRP-protected) for performance, so its unwind write is unchecked. base::WeakAutoReset instead holds a base::WeakPtr to the owning object and skips the restore write if the object has been destroyed.
`GetInputMethod()->OnCaretBoundsChanged(this)`
On Windows this reaches TSFTextStore::SendOnLayoutChange, a synchronous COM callout into the active third-party Text Services Framework (TSF) IME.
Nested message pumping
During a user move/resize, ScopedAllowApplicationTasksInNativeNestedLoop is active; if the IME pumps the Windows message loop inside the COM callout, Chrome processes pending tasks, including Mojo IPC.
Self-owned popup widgets
A popup’s RenderWidgetHostImpl/RenderWidgetHostViewAura can be destroyed by a blink::mojom::PopupWidgetHost::RequestClosePopup IPC from the renderer, i.e. re-entrantly during the callout.
`weak_ptr_factory_`
The view’s own weak-pointer factory, used by the fix to detect self-destruction across the callout.

Root Cause Analysis

The invariant violated is that this (and its in_bounds_changed_ member) must stay alive for the whole lexical scope of OnBoundsChanged. The old code opened the function with base::AutoReset<bool> in_bounds_changed(&in_bounds_changed_, true); and later, inside if (GetInputMethod()) { GetInputMethod()->OnCaretBoundsChanged(this); UpdateInsetsWithVirtualKeyboardEnabled(); }. On Windows, OnCaretBoundsChanged makes a synchronous COM callout to a third-party TSF IME; if that IME pumps the message queue (legal while ScopedAllowApplicationTasksInNativeNestedLoop is active during a resize), a queued RequestClosePopup Mojo task can synchronously delete the self-owned view while OnBoundsChanged is still on the stack. When the callout returns, the function (1) calls UpdateInsetsWithVirtualKeyboardEnabled() on the freed this (a use-after-free read of freed members such as keyboard_occluded_bounds_), and (2) on unwind, ~AutoReset<bool> writes the saved false (0x00) back through its RAW_PTR_EXCLUSION pointer into in_bounds_changed_ in the freed object — an unchecked 1-byte write-after-free.

The patch makes two changes. First, it replaces base::AutoReset<bool> with base::WeakAutoReset in_bounds_changed(weak_ptr_factory_.GetWeakPtr(), &RenderWidgetHostViewAura::in_bounds_changed_, true); so the destructor’s restore write is suppressed if the view was destroyed — closing the write-after-free that AutoReset’s BRP-excluded pointer left unprotected. Second, it captures auto weak_this = weak_ptr_factory_.GetWeakPtr(); immediately before OnCaretBoundsChanged(this) and, after the callout, does if (!weak_this) { return; } before UpdateInsetsWithVirtualKeyboardEnabled() — closing the use-after-free read. The new test DestroyDuringOnCaretBoundsChanged installs a DestroyingMockInputMethod that destroys the view inside OnCaretBoundsChanged, reproducing the ASAN heap-use-after-free (read) and write-after-free (in ~AutoReset) pre-fix. The advisory’s CWE-416 “Use after free in UI” is accurate; the diff shows both a UAF read and a WAF on unwind.

Key insight
A stack base::AutoReset and a post-callout member access both assumed this outlived a synchronous third-party IME COM callout that can re-entrantly free the view — and AutoReset’s restore pointer is RAW_PTR_EXCLUSION, so even MiraclePtr would not catch the unwind write.

Attack Path

  1. Compromise a renderer Gain code execution in a sandboxed renderer (assumed starting point).
  2. Create a self-owned popup Open a popup window, creating a self-owned RenderWidgetHostImpl/RenderWidgetHostViewAura in the browser that a RequestClosePopup IPC can destroy.
  3. Enter `OnBoundsChanged` Induce a window bounds change (resize) so the view constructs the stack AutoReset and calls GetInputMethod()->OnCaretBoundsChanged(this), reaching the TSF IME COM callout (Windows, third-party IME pumping messages — a real-world precondition, partly inferred).
  4. Re-entrant destroy Deliver a blink::mojom::PopupWidgetHost::RequestClosePopup Mojo IPC timed so it runs while the IME pumps the loop, synchronously deleting the view mid-callout.
  5. Reclaim the slot Spray browser-heap allocations to reclaim the freed RenderWidgetHostViewAura memory with controlled bytes (graceful teardown clears raw_ptrs first, so BRP does not quarantine the slot — reclamation is reliable; inferred).
  6. UAF/WAF primitive On unwind, the freed object is read in UpdateInsetsWithVirtualKeyboardEnabled() and the ~AutoReset 0x00 write lands in the freed/reclaimed slot at a fixed offset — a controlled-adjacency write-after-free leveragable toward sandbox escape/RCE (escalation inferred; diff proves the UAF/WAF).

Impact Assessment

This is a browser-process (unsandboxed) use-after-free + write-after-free reachable from a compromised renderer, hence a sandbox-escape vector (CVSS S:C). It is Windows-specific and gated on a third-party TSF IME being active and pumping the message loop during a resize, plus winning the race to deliver the close-popup IPC during the COM callout (AC:H/UI:R). The diff proves a read UAF (in UpdateInsetsWithVirtualKeyboardEnabled) and a 1-byte unchecked WAF (in ~AutoReset); turning the 1-byte/adjacent-corruption write into full RCE is plausible but inferred and not shown. The RAW_PTR_EXCLUSION on AutoReset::scoped_variable_ means MiraclePtr does not mitigate the unwind write.

Changed Functions

FunctionChangeNotes
if
content/browser/renderer_host/render_widget_host_view_aura.cc
modified
DestroyingMockInputMethod
content/browser/renderer_host/render_widget_host_view_aura_unittest.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
MockVirtualKeyboardController
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

Audit Directions

  • `base::AutoReset` across re-entrant callouts
    Audit every base::AutoReset/AutoReset-guarded member set whose scope spans a synchronous callout that can delete the owning object (IME, accessibility, drag/drop, nested run loops); these are candidates for WeakAutoReset.
  • RAW_PTR_EXCLUSION restore pointers
    Review other utilities (scoped setters, restorers) whose back-pointer is BRP-excluded and that write on unwind, since MiraclePtr will not catch those UAFs.
  • Synchronous IME / COM callouts in the browser
    Trace other GetInputMethod()/TSF callouts (OnTextInputStateChanged, layout/selection updates) that pass this and then continue to use members after return.
  • Self-owned widget destruction during browser work
    Check other browser code holding a popup/self-owned RenderWidgetHost* across message-pump re-entrancy where RequestClosePopup-style IPC can delete it.
From a66bf3aeca0d239b36e6e54487d91454d05bb486 Mon Sep 17 00:00:00 2001
From: Jonathan Ross <jonross@chromium.org>
Date: Thu, 07 May 2026 11:56:41 -0700
Subject: [PATCH] Fix Use-After-Free in RenderWidgetHostViewAura::OnBoundsChanged

On Windows, RenderWidgetHostViewAura::OnBoundsChanged() calls
GetInputMethod()->OnCaretBoundsChanged(this), which can trigger a
synchronous COM callout to a third-party TSF IME. If the IME pumps the
thread message queue, a pending task (such as a popup close IPC) can
synchronously destroy the view while OnBoundsChanged() is still on the
stack.

This leads to a Use-After-Free when UpdateInsetsWithVirtualKeyboardEnabled()
is called after the IME callout returns, and a Write-After-Free when the
stack-allocated base::AutoReset<bool> destructor attempts to reset
in_bounds_changed_ in the freed object.

This CL fixes the issue by:
1. Replacing base::AutoReset with base::WeakAutoReset to safely handle
   object destruction during the reset scope.
2. Adding a WeakPtr check after the IME callout to skip further member
   access if the object was destroyed.

Bug: 502112506
Change-Id: Id64cc95569c98a4c62318b7f86e11d34365efdfe
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7819560
Reviewed-by: Aman Verma <amanvr@google.com>
Commit-Queue: Jonathan Ross <jonross@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1627112}
---

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 b6f9361..9903327 100644
--- a/content/browser/renderer_host/render_widget_host_view_aura.cc
+++ b/content/browser/renderer_host/render_widget_host_view_aura.cc
@@ -16,6 +16,7 @@
 #include "base/functional/callback_helpers.h"
 #include "base/logging.h"
 #include "base/memory/raw_ptr.h"
+#include "base/memory/weak_auto_reset.h"
 #include "base/metrics/histogram_functions.h"
 #include "base/notimplemented.h"
 #include "base/strings/strcat.h"
@@ -2235,7 +2236,13 @@
 
 void RenderWidgetHostViewAura::OnBoundsChanged(const gfx::Rect& old_bounds,
                                                const gfx::Rect& new_bounds) {
-  base::AutoReset<bool> in_bounds_changed(&in_bounds_changed_, true);
+  // OnCaretBoundsChanged() below may call out to a third-party TSF IME on
+  // Windows, which can re-entrantly destroy `this`. Use WeakAutoReset so the
+  // unwind write does not land in freed memory (AutoReset::scoped_variable_ is
+  // RAW_PTR_EXCLUSION and not BRP-protected).
+  base::WeakAutoReset in_bounds_changed(
+      weak_ptr_factory_.GetWeakPtr(),
+      &RenderWidgetHostViewAura::in_bounds_changed_, true);
   // We care about this whenever RenderWidgetHostViewAura is not owned by a
   // WebContentsViewAura since changes to the Window's bounds need to be
   // messaged to the renderer.  WebContentsViewAura invokes SetSize() or
@@ -2244,7 +2251,12 @@
   SetSize(new_bounds.size());
 
   if (GetInputMethod()) {
+    auto weak_this = weak_ptr_factory_.GetWeakPtr();
     GetInputMethod()->OnCaretBoundsChanged(this);
+    // `this` may have been deleted inside the IME callout.
+    if (!weak_this) {
+      return;
+    }
     UpdateInsetsWithVirtualKeyboardEnabled();
   }
 }
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 0a69d55..feb608c4 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
@@ -6861,6 +6861,84 @@
   GetInputMethod()->RemoveObserver(this);
 }
 
+// Mock InputMethod that runs a closure when OnCaretBoundsChanged is invoked.
+// Simulates a Windows TSF IME whose ITextStoreACPSink::OnLayoutChange handler
+// pumps the thread message queue, allowing a queued task to synchronously
+// destroy the RenderWidgetHostViewAura while it is still inside
+// OnBoundsChanged().
+class DestroyingMockInputMethod : public ui::MockInputMethod {
+ public:
+  DestroyingMockInputMethod() : ui::MockInputMethod(nullptr) {}
+
+  void OnCaretBoundsChanged(const ui::TextInputClient* client) override {
+    ui::MockInputMethod::OnCaretBoundsChanged(client);
+    if (on_caret_bounds_changed_) {
+      std::move(on_caret_bounds_changed_).Run();
+    }
+  }
+
+  void set_on_caret_bounds_changed(base::OnceClosure closure) {
+    on_caret_bounds_changed_ = std::move(closure);
+  }
+
+ private:
+  base::OnceClosure on_caret_bounds_changed_;
+};
+
+class RenderWidgetHostViewAuraOnBoundsChangedUAFTest
+    : public RenderWidgetHostViewAuraTest {
+ public:
+  void SetUp() override {
+    input_method_ = new DestroyingMockInputMethod();
+    // Ownership is transferred to the WindowTreeHost via the InputMethod
+    // factory; see SetUpInputMethodForTesting().
+    ui::SetUpInputMethodForTesting(input_method_);
+    SetUpEnvironment();
+  }
+
+  void TearDown() override {
+    input_method_ = nullptr;
+    RenderWidgetHostViewAuraTest::TearDown();
+  }
+
+ protected:
+  raw_ptr<DestroyingMockInputMethod> input_method_ = nullptr;
+};
+
+// RWHVA::OnBoundsChanged() constructs a base::AutoReset<bool> holding
+// &in_bounds_changed_, then calls GetInputMethod()->OnCaretBoundsChanged(this).
+// On Windows that reaches TSFTextStore::SendOnLayoutChange ->
+// text_store_acp_sink_->OnLayoutChange(), a synchronous COM call into the
+// active third-party IME. If the IME pumps messages and the view is destroyed
+// re-entrantly, on unwind UpdateInsetsWithVirtualKeyboardEnabled() and
+// ~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,
+       DestroyDuringOnCaretBoundsChanged) {
+  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
+  // OnCaretBoundsChanged, simulating re-entrant destruction triggered by a
+  // TSF IME callout that pumps a queued window.close() / renderer-gone task.
+  FakeRenderWidgetHostViewAura* raw_view = view_.get();
+  input_method_->set_on_caret_bounds_changed(base::BindLambdaForTesting([&]() {
+    widget_host_ = nullptr;
+    view_.ExtractAsDangling()->Destroy();
+  }));
+
+  // Under ASAN this triggers heap-use-after-free in
+  // UpdateInsetsWithVirtualKeyboardEnabled() (read of freed
+  // keyboard_occluded_bounds_) followed by a write-after-free in
+  // ~AutoReset<bool> to the freed in_bounds_changed_ slot.
+  raw_view->OnBoundsChanged(gfx::Rect(), gfx::Rect(0, 0, 100, 100));
+}
+
 #if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_CHROMEOS)
 class MockVirtualKeyboardController final
     : public ui::VirtualKeyboardController {
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 0a69d55..feb608c4 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
@@ -6861,6 +6861,84 @@
   GetInputMethod()->RemoveObserver(this);
 }
 
+// Mock InputMethod that runs a closure when OnCaretBoundsChanged is invoked.
+// Simulates a Windows TSF IME whose ITextStoreACPSink::OnLayoutChange handler
+// pumps the thread message queue, allowing a queued task to synchronously
+// destroy the RenderWidgetHostViewAura while it is still inside
+// OnBoundsChanged().
+class DestroyingMockInputMethod : public ui::MockInputMethod {
+ public:
+  DestroyingMockInputMethod() : ui::MockInputMethod(nullptr) {}
+
+  void OnCaretBoundsChanged(const ui::TextInputClient* client) override {
+    ui::MockInputMethod::OnCaretBoundsChanged(client);
+    if (on_caret_bounds_changed_) {
+      std::move(on_caret_bounds_changed_).Run();
+    }
+  }
+
+  void set_on_caret_bounds_changed(base::OnceClosure closure) {
+    on_caret_bounds_changed_ = std::move(closure);
+  }
+
+ private:
+  base::OnceClosure on_caret_bounds_changed_;
+};
+
+class RenderWidgetHostViewAuraOnBoundsChangedUAFTest
+    : public RenderWidgetHostViewAuraTest {
+ public:
+  void SetUp() override {
+    input_method_ = new DestroyingMockInputMethod();
+    // Ownership is transferred to the WindowTreeHost via the InputMethod
+    // factory; see SetUpInputMethodForTesting().
+    ui::SetUpInputMethodForTesting(input_method_);
+    SetUpEnvironment();
+  }
+
+  void TearDown() override {
+    input_method_ = nullptr;
+    RenderWidgetHostViewAuraTest::TearDown();
+  }
+
+ protected:
+  raw_ptr<DestroyingMockInputMethod> input_method_ = nullptr;
+};
+
+// RWHVA::OnBoundsChanged() constructs a base::AutoReset<bool> holding
+// &in_bounds_changed_, then calls GetInputMethod()->OnCaretBoundsChanged(this).
+// On Windows that reaches TSFTextStore::SendOnLayoutChange ->
+// text_store_acp_sink_->OnLayoutChange(), a synchronous COM call into the
+// active third-party IME. If the IME pumps messages and the view is destroyed
+// re-entrantly, on unwind UpdateInsetsWithVirtualKeyboardEnabled() and
+// ~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,
+       DestroyDuringOnCaretBoundsChanged) {
+  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
+  // OnCaretBoundsChanged, simulating re-entrant destruction triggered by a
+  // TSF IME callout that pumps a queued window.close() / renderer-gone task.
+  FakeRenderWidgetHostViewAura* raw_view = view_.get();
+  input_method_->set_on_caret_bounds_changed(base::BindLambdaForTesting([&]() {
+    widget_host_ = nullptr;
+    view_.ExtractAsDangling()->Destroy();
+  }));
+
+  // Under ASAN this triggers heap-use-after-free in
+  // UpdateInsetsWithVirtualKeyboardEnabled() (read of freed
+  // keyboard_occluded_bounds_) followed by a write-after-free in
+  // ~AutoReset<bool> to the freed in_bounds_changed_ slot.
+  raw_view->OnBoundsChanged(gfx::Rect(), gfx::Rect(0, 0, 100, 100));
+}
+
 #if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_CHROMEOS)
 class MockVirtualKeyboardController final
     : public ui::VirtualKeyboardController {
Loading diff…

Original Bug Report

reported by vm...@google.com

Potential UAF/WAF in RenderWidgetHostViewAura via TSF IME COM callout

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.

Overview: A potential Use-After-Free and Write-After-Free exists in RenderWidgetHostViewAura::OnBoundsChanged on Windows. A synchronous COM callout to a TSF IME during window resizing allows nested task execution, enabling a compromised renderer to synchronously destroy the view via a popup close IPC. This bypasses MiraclePtr, leading to an unprotected 1-byte write-after-free when a stack-allocated base::AutoReset is destroyed.

Affected files:

  • content/browser/renderer_host/render_widget_host_view_aura.cc
  • ui/base/ime/win/tsf_text_store.cc
  • ui/base/ime/win/input_method_win_tsf.cc
  • ui/base/ime/win/tsf_bridge.cc

Estimated timestamp from git blame: 2021-10-07

Description

In content/browser/renderer_host/render_widget_host_view_aura.cc, the OnBoundsChanged method utilizes a stack-allocated base::AutoReset<bool> to manage the in_bounds_changed_ state. It subsequently calls GetInputMethod()->OnCaretBoundsChanged(this). On Windows, this triggers a synchronous COM callout to the active third-party Text Services Framework (TSF) IME via TSFTextStore::SendOnLayoutChange.

During a user-initiated window move or resize, ScopedAllowApplicationTasksInNativeNestedLoop is active (instantiated in HWNDMessageHandler). If the third-party IME pumps the Windows message loop during the synchronous COM callout, Chrome will process pending tasks from its queues, including Mojo IPC messages.

Because popup widgets are self-owned, a compromised renderer can send a blink::mojom::PopupWidgetHost::RequestClosePopup IPC to synchronously delete the RenderWidgetHostImpl and its associated RenderWidgetHostViewAura while OnBoundsChanged is still on the stack.

When the COM callout returns, the stack unwinds into the deleted object. The base::AutoReset destructor attempts to write false (0x00) back to the in_bounds_changed_ member variable. Crucially, base::AutoReset’s internal pointer uses RAW_PTR_EXCLUSION for performance reasons. This results in an unprotected 1-byte Write-After-Free at a fixed offset.

MiraclePtr Bypass

BackupRefPtr (MiraclePtr) typically protects against UAFs by quarantining freed memory if raw_ptr references still exist. However, during normal teardown, Chrome gracefully clears raw_ptr references (such as deleting the aura::Window and detaching the input method delegate) before RenderWidgetHostViewAura is actually freed. Because the raw_ptr reference count reaches zero prior to the memory being freed, the memory is physically returned to the allocator and is not quarantined. This allows an attacker to reliably reclaim the memory before the stack unwinds.

Potential Attacker Steps

  1. Compromise a renderer process to gain arbitrary code execution within the sandbox.
  2. Open a popup window, creating a self-owned RenderWidgetHostImpl in the browser process.
  3. Induce a window bounds change (e.g., resizing) to enter RenderWidgetHostViewAura::OnBoundsChanged.
  4. Send a RequestClosePopup Mojo IPC to the browser process precisely when the TSF IME is pumping the message loop during the COM callout.
  5. Send additional IPCs to spray the browser process heap, reclaiming the freed RenderWidgetHostViewAura memory with controlled data (e.g., null bytes).
  6. When the stack unwinds, UpdateInsetsWithVirtualKeyboardEnabled() executes safely using the controlled data, followed by the unprotected 0x00 write-after-free from ~AutoReset, which can be leveraged to corrupt adjacent structures for a Sandbox Escape / RCE.

Note: Please note that these are suggested/potential steps; our tooling agent doesn’t yet have the ability to run code to produce a working proof-of-concept.

Suggested Fix

The base::AutoReset class assumes the underlying object will outlive the scope. To resolve this, avoid using base::AutoReset when synchronous callouts could lead to object destruction. Instead, capture a base::WeakPtr<RenderWidgetHostViewAura> and manually check its validity after the COM callout returns before accessing this or updating any member variables.

Evaluated with Chrome root at commit: 096fc8fdbfacf2546485756d03f160a3d04fcc9b


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