CVE-2026-9937
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifcontent/browser/renderer_host/render_widget_host_view_aura.cc |
modified | |
DestroyingMockInputMethodcontent/browser/renderer_host/render_widget_host_view_aura_unittest.cc |
modified | |
ifcontent/browser/renderer_host/render_widget_host_view_aura_unittest.cc |
modified | |
RenderWidgetHostViewAuraOnBoundsChangedUAFTestcontent/browser/renderer_host/render_widget_host_view_aura_unittest.cc |
modified | |
MockVirtualKeyboardControllercontent/browser/renderer_host/render_widget_host_view_aura_unittest.cc |
modified |
Files Changed
content/browser/renderer_host/render_widget_host_view_aura.cccontent/browser/renderer_host/render_widget_host_view_aura_unittest.cc
Patch
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 {
Regression Test / PoC
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 {
Original Bug Report
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.ccui/base/ime/win/tsf_text_store.ccui/base/ime/win/input_method_win_tsf.ccui/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
- Compromise a renderer process to gain arbitrary code execution within the sandbox.
- Open a popup window, creating a self-owned
RenderWidgetHostImplin the browser process. - Induce a window bounds change (e.g., resizing) to enter
RenderWidgetHostViewAura::OnBoundsChanged. - Send a
RequestClosePopupMojo IPC to the browser process precisely when the TSF IME is pumping the message loop during the COM callout. - Send additional IPCs to spray the browser process heap, reclaiming the freed
RenderWidgetHostViewAuramemory with controlled data (e.g., null bytes). - When the stack unwinds,
UpdateInsetsWithVirtualKeyboardEnabled()executes safely using the controlled data, followed by the unprotected0x00write-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.