CVE-2026-15120
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifcontent/browser/renderer_host/render_widget_host_view_aura.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 |
Files Changed
content/browser/renderer_host/render_widget_host_view_aura.cccontent/browser/renderer_host/render_widget_host_view_aura_unittest.cc
Patch
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
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 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 {
Original Bug Report
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.
- Gain Initial Execution: An attacker first compromises a renderer process (e.g., via a v8 vulnerability).
- Trigger State Update: The compromised renderer crafts and sends a
mojom::WidgetHost::TextInputStateChangedIPC message to the browser process to update its text input state. - Concurrent Destruction: Concurrently, the attacker triggers a window or tab closure event (e.g., via a secondary compromised renderer or Web Worker IPC).
- Message Loop Pumping: The
TextInputStateChangedIPC reachesRenderWidgetHostViewAura::OnUpdateTextInputStateCalled, triggering the synchronousOnTextInputTypeChangedIME call. This pumps the message loop, processing the closure event and synchronously destroying theRenderWidgetHostViewAuraobject. - Memory Reallocation: The attacker uses heap grooming/spraying in the browser process to immediately reallocate the freed
RenderWidgetHostViewAuramemory chunk with a fake object. This fake object is crafted to ensure thehost_pointer points to another fakeRenderWidgetHostImplobject under the attacker’s control. - Control Flow Hijack: The IME call returns. The browser uses the raw
updated_viewpointer to read the attacker-controlledhost_pointer and callsRequestCompositionUpdates. This triggers a virtual method call (GetWidgetInputHandler()) on the fakeRenderWidgetHostImpl, 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.