CVE-2026-9937
Overview
Background
- `base::AutoReset<bool>` vs `base::WeakAutoReset`
AutoResetstores a raw pointer to a member and writes a saved value back on destruction. Itsscoped_variable_pointer isRAW_PTR_EXCLUSION(not BRP-protected) for performance, so its unwind write is unchecked.base::WeakAutoResetinstead holds abase::WeakPtrto 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,
ScopedAllowApplicationTasksInNativeNestedLoopis 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/RenderWidgetHostViewAuracan be destroyed by ablink::mojom::PopupWidgetHost::RequestClosePopupIPC 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.
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
- Compromise a renderer Gain code execution in a sandboxed renderer (assumed starting point).
- Create a self-owned popup
Open a popup window, creating a self-owned
RenderWidgetHostImpl/RenderWidgetHostViewAurain the browser that aRequestClosePopupIPC can destroy. - Enter `OnBoundsChanged`
Induce a window bounds change (resize) so the view constructs the stack
AutoResetand callsGetInputMethod()->OnCaretBoundsChanged(this), reaching the TSF IME COM callout (Windows, third-party IME pumping messages — a real-world precondition, partly inferred). - Re-entrant destroy
Deliver a
blink::mojom::PopupWidgetHost::RequestClosePopupMojo IPC timed so it runs while the IME pumps the loop, synchronously deleting the view mid-callout. - Reclaim the slot
Spray browser-heap allocations to reclaim the freed
RenderWidgetHostViewAuramemory with controlled bytes (graceful teardown clearsraw_ptrs first, so BRP does not quarantine the slot — reclamation is reliable; inferred). - UAF/WAF primitive
On unwind, the freed object is read in
UpdateInsetsWithVirtualKeyboardEnabled()and the~AutoReset0x00 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
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
| 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
Audit Directions
- `base::AutoReset` across re-entrant calloutsAudit 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 forWeakAutoReset. - RAW_PTR_EXCLUSION restore pointersReview 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 browserTrace other
GetInputMethod()/TSF callouts (OnTextInputStateChanged, layout/selection updates) that passthisand then continue to use members after return. - Self-owned widget destruction during browser workCheck other browser code holding a popup/self-owned
RenderWidgetHost*across message-pump re-entrancy whereRequestClosePopup-style IPC can delete it.
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.