CVE-2026-91718
Overview
Background
- `RenderWidgetHostViewAura` (RWHVA)
- the Aura-platform browser-process view object that hosts a renderer widget and mediates IME/text-input interactions with the platform input method.
- IME callout
- a call from the view into
GetInputMethod()(e.g.OnTextInputTypeChanged,SetVirtualKeyboardVisibilityIfEnabled,NotifySelectionBoundsChanged) that can synchronously run observers, message pumps, or the ChromeOS keyboard show/hide cascade and thus re-enter browser code. - `TextInputManager` / `state`
- the manager that tracks the active view’s text-input state;
stateis a raw pointer returned bytext_input_manager_->GetTextInputState()that is owned by, and freed with, the registered active view’s tracked entry. - `base::WeakPtr`
- a pointer that automatically nulls when its referent is destroyed, used here to detect re-entrant deletion of
thisor ofupdated_view.
Root Cause Analysis
The edited function (identified from the shipped test comment as RenderWidgetHostViewAura::OnUpdateTextInputStateCalled, (inferred) since the name is not in the hunk context) caches a raw state pointer from text_input_manager_->GetTextInputState() and then performs several IME callouts. Each of these callouts — OnTextInputTypeChanged, SetVirtualKeyboardVisibilityIfEnabled(false/true), virtual_keyboard_controller_win_->UpdateTextInputState, and NotifySelectionBoundsChanged — can re-entrantly run code that destroys the view (this or updated_view) or unregisters/updates the active view, freeing the object state points at.
Pre-patch, the function only re-checked weak_this after two of the callouts and never re-fetched state, so after a re-entrant destruction execution continued to dereference the freed state (e.g. state->type != ui::TEXT_INPUT_TYPE_NONE) and the freed updated_view (updated_view->host()).
The fix introduces a check_alive() lambda that validates weak_this, a new weak_updated_view, a non-null text_input_manager_, and that updated_view is still registered, invoking it after every callout and re-fetching state = text_input_manager_->GetTextInputState() so no stale pointer survives a re-entrant mutation.
The patch strictly TIGHTENS the liveness checks: it replaces the weak_this-only guard with a broader multi-condition guard and adds guards on the previously-unchecked virtual-keyboard callouts.
state pointer and a this-only liveness check across IME callouts that can re-entrantly free both the view and the text-input state. The fix converts every callout into a checkpoint that revalidates all involved objects and re-reads state, so a stale/freed pointer is never dereferenced.Attack Path
- Reach the text-input update path
The renderer sends a text-input state update (type
TEXT_INPUT_TYPE_TEXTwithshow_ime_if_neededset), drivingTextInputManager::UpdateTextInputStateinto the view’sOnUpdateTextInputStateCalled, which cachesstateand enters the virtual-keyboard branch. - Trigger a re-entrant IME callout
Because the view is the focused text input client, the function calls
GetInputMethod()->SetVirtualKeyboardVisibilityIfEnabled(true), which on Aura/ChromeOS runs the keyboard visibility cascade (observers / message pump). - Destroy the view during the callout
That cascade synchronously destroys
updated_view/this(the regression test models this by destroyingview_insideSetVirtualKeyboardVisibilityIfEnabled), freeing the RWHVA and theTextInputStateobjectstatereferenced. - Use-after-free on unwind
Control returns to the (pre-patch) function, which dereferences the freed
state(state->type) and later the freedupdated_view(updated_view->host()), a use-after-free in the browser process. - Escalation
Turning this dangling read/access into a controlled read/write or code execution
(inferred)— the patch demonstrates only the dangling dereference, not a specific corruption primitive.
Impact Assessment
RenderWidgetHostViewAura and its TextInputState in the privileged browser process. Preconditions include the view being the focused text-input client and an environment where the virtual-keyboard callout re-enters and destroys the view (the test cites ChromeOS’s KeyboardUIController show/hide cascade). The advisory classifies this as a UAF in Core; any escalation to a controlled memory-corruption primitive or code execution is (inferred) and not established by the patch, and no exploited-in-the-wild status is stated.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 | |
BindLambdaForTestingcontent/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
- Cached owner-owned pointers across re-entrant calloutsAudit any function that caches a raw pointer (like
state) obtained from a manager and then calls out to code that can mutate/free the owner’s entry; re-fetch after every such call rather than reusing the cached value. - Partial liveness checksLook for
WeakPtrguards that validate onlythiswhile other objects (updated_view,text_input_manager_) used later on the same path can also be freed; every object dereferenced after a callout needs its own liveness/registration check. - IME / virtual-keyboard callout chainsReview other
GetInputMethod()andTextInputManagercallouts (e.g.NotifySelectionBoundsChanged,OnTextInputTypeChanged,UpdateTextInputState) and platform keyboard controllers that can synchronously re-enter and destroy views, especially on Aura/ChromeOS.
Patch
From 77d3b674597221f343a95f95caaf04f1c1a66ea8 Mon Sep 17 00:00:00 2001
From: Dave Tapuska <dtapuska@chromium.org>
Date: Wed, 09 Sep 2026 08:20:50 -0700
Subject: [PATCH] Prevent UAF during re-entrant destruction in IME callouts.
During IME callouts (such as SetVirtualKeyboardVisibilityIfEnabled or
NotifySelectionBoundsChanged), the RenderWidgetHostViewAura or the
updated view can be re-entrantly destroyed or unregistered. This CL adds
weak pointer checks and re-fetches the text input state after these
callouts to prevent accessing freed memory. A unit test is added to
verify the fix when destruction occurs during virtual keyboard
visibility updates.
Bug: 553133215
Change-Id: I4a6fc2c50133c731dad652c07906e6aa597c82dc
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8351288
Reviewed-by: Kartar Singh <kartarsingh@google.com>
Commit-Queue: Dave Tapuska <dtapuska@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1694603}
---
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 277febb..f839fc1 100644
--- a/content/browser/renderer_host/render_widget_host_view_aura.cc
+++ b/content/browser/renderer_host/render_widget_host_view_aura.cc
@@ -3503,15 +3503,26 @@
bool did_update_state) {
CHECK_EQ(text_input_manager_, text_input_manager);
- if (!GetInputMethod())
+ if (!GetInputMethod()) {
return;
+ }
auto weak_this = weak_ptr_factory_.GetWeakPtr();
+ base::WeakPtr<RenderWidgetHostViewBase> weak_updated_view =
+ updated_view ? updated_view->GetWeakPtr() : nullptr;
+
+ auto check_alive = [&]() -> bool {
+ if (!weak_this || !weak_updated_view || !text_input_manager_ ||
+ !text_input_manager_->IsRegistered(updated_view)) {
+ // `this` or `updated_view` may have been deleted inside the IME callout.
+ return false;
+ }
+ return true;
+ };
if (did_update_state) {
GetInputMethod()->OnTextInputTypeChanged(this);
- if (!weak_this) {
- // `this` may have been deleted inside the IME callout.
+ if (!check_alive()) {
return;
}
}
@@ -3528,6 +3539,12 @@
ui::mojom::VirtualKeyboardVisibilityRequest::HIDE) {
GetInputMethod()->SetVirtualKeyboardVisibilityIfEnabled(false);
}
+ if (!check_alive()) {
+ return;
+ }
+ // The IME callout may have re-entrantly updated or unregistered the
+ // active view's state, freeing the object `state` points to.
+ state = text_input_manager_->GetTextInputState();
}
#endif
@@ -3538,6 +3555,12 @@
if (state->show_ime_if_needed &&
GetInputMethod()->GetTextInputClient() == this) {
GetInputMethod()->SetVirtualKeyboardVisibilityIfEnabled(true);
+ if (!check_alive()) {
+ return;
+ }
+ // The IME callout may have re-entrantly updated or unregistered the
+ // active view's state, freeing the object `state` points to.
+ state = text_input_manager_->GetTextInputState();
}
// TODO(crbug.com/40110609): Remove this once TSF fix for input pane policy
// is serviced
@@ -3549,6 +3572,12 @@
GetInputMethod());
}
virtual_keyboard_controller_win_->UpdateTextInputState(state);
+ if (!check_alive()) {
+ return;
+ }
+ // The IME callout may have re-entrantly updated or unregistered the
+ // active view's state, freeing the object `state` points to.
+ state = text_input_manager_->GetTextInputState();
}
#endif
}
@@ -3556,10 +3585,12 @@
// 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.
+ if (!check_alive()) {
return;
}
+ // The IME callout may have re-entrantly updated or unregistered the
+ // active view's state, freeing the object `state` points to.
+ state = text_input_manager_->GetTextInputState();
}
if (auto* render_widget_host = updated_view->host()) {
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 33ed433..b74f54f 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
@@ -7045,6 +7045,13 @@
}
}
+ void SetVirtualKeyboardVisibilityIfEnabled(bool should_show) override {
+ ui::MockInputMethod::SetVirtualKeyboardVisibilityIfEnabled(should_show);
+ if (on_set_virtual_keyboard_visibility_if_enabled_) {
+ std::move(on_set_virtual_keyboard_visibility_if_enabled_).Run();
+ }
+ }
+
void DetachTextInputClient(ui::TextInputClient* client) override {
ui::MockInputMethod::DetachTextInputClient(client);
if (on_detach_text_input_client_) {
@@ -7060,6 +7067,11 @@
on_text_input_type_changed_ = std::move(closure);
}
+ void set_on_set_virtual_keyboard_visibility_if_enabled(
+ base::OnceClosure closure) {
+ on_set_virtual_keyboard_visibility_if_enabled_ = std::move(closure);
+ }
+
void set_on_detach_text_input_client(base::OnceClosure closure) {
on_detach_text_input_client_ = std::move(closure);
}
@@ -7067,6 +7079,7 @@
private:
base::OnceClosure on_caret_bounds_changed_;
base::OnceClosure on_text_input_type_changed_;
+ base::OnceClosure on_set_virtual_keyboard_visibility_if_enabled_;
base::OnceClosure on_detach_text_input_client_;
};
@@ -7155,6 +7168,41 @@
GetTextInputManager(view_)->UpdateTextInputState(view_, state);
}
+// RWHVA::OnUpdateTextInputStateCalled() calls
+// GetInputMethod()->SetVirtualKeyboardVisibilityIfEnabled(true) when the state
+// indicates that the virtual keyboard should be shown. If an observer or
+// virtual keyboard controller pumps messages and the view is destroyed
+// re-entrantly (e.g. on ChromeOS where KeyboardUIController runs the keyboard
+// show/hide cascade), on unwind the function must not dereference freed view
+// pointers or stale text input state.
+TEST_F(RenderWidgetHostViewAuraReentrantDestructionIME,
+ DestroyDuringSetVirtualKeyboardVisibilityIfEnabled) {
+ 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());
+
+ // The virtual-keyboard branch requires the view to be the InputMethod's
+ // focused text input client.
+ input_method_->SetFocusedTextInputClient(view_.get());
+
+ // Arrange for the view to be synchronously destroyed inside
+ // SetVirtualKeyboardVisibilityIfEnabled.
+ input_method_->set_on_set_virtual_keyboard_visibility_if_enabled(
+ base::BindLambdaForTesting([&]() {
+ widget_host_ = nullptr;
+ view_.ExtractAsDangling()->Destroy();
+ }));
+
+ ui::mojom::TextInputState state;
+ state.type = ui::TEXT_INPUT_TYPE_TEXT;
+ state.mode = ui::TEXT_INPUT_MODE_TEXT;
+ state.show_ime_if_needed = true;
+ GetTextInputManager(view_.get())->UpdateTextInputState(view_.get(), state);
+}
+
// RWHVA::OnWindowFocused()'s lost-focus branch calls
// DetachFromInputMethod(false). On Windows this reaches
// TSFBridge::RemoveFocusedClient() -> ITfThreadMgr::AssociateFocus(), a
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 33ed433..b74f54f 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
@@ -7045,6 +7045,13 @@
}
}
+ void SetVirtualKeyboardVisibilityIfEnabled(bool should_show) override {
+ ui::MockInputMethod::SetVirtualKeyboardVisibilityIfEnabled(should_show);
+ if (on_set_virtual_keyboard_visibility_if_enabled_) {
+ std::move(on_set_virtual_keyboard_visibility_if_enabled_).Run();
+ }
+ }
+
void DetachTextInputClient(ui::TextInputClient* client) override {
ui::MockInputMethod::DetachTextInputClient(client);
if (on_detach_text_input_client_) {
@@ -7060,6 +7067,11 @@
on_text_input_type_changed_ = std::move(closure);
}
+ void set_on_set_virtual_keyboard_visibility_if_enabled(
+ base::OnceClosure closure) {
+ on_set_virtual_keyboard_visibility_if_enabled_ = std::move(closure);
+ }
+
void set_on_detach_text_input_client(base::OnceClosure closure) {
on_detach_text_input_client_ = std::move(closure);
}
@@ -7067,6 +7079,7 @@
private:
base::OnceClosure on_caret_bounds_changed_;
base::OnceClosure on_text_input_type_changed_;
+ base::OnceClosure on_set_virtual_keyboard_visibility_if_enabled_;
base::OnceClosure on_detach_text_input_client_;
};
@@ -7155,6 +7168,41 @@
GetTextInputManager(view_)->UpdateTextInputState(view_, state);
}
+// RWHVA::OnUpdateTextInputStateCalled() calls
+// GetInputMethod()->SetVirtualKeyboardVisibilityIfEnabled(true) when the state
+// indicates that the virtual keyboard should be shown. If an observer or
+// virtual keyboard controller pumps messages and the view is destroyed
+// re-entrantly (e.g. on ChromeOS where KeyboardUIController runs the keyboard
+// show/hide cascade), on unwind the function must not dereference freed view
+// pointers or stale text input state.
+TEST_F(RenderWidgetHostViewAuraReentrantDestructionIME,
+ DestroyDuringSetVirtualKeyboardVisibilityIfEnabled) {
+ 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());
+
+ // The virtual-keyboard branch requires the view to be the InputMethod's
+ // focused text input client.
+ input_method_->SetFocusedTextInputClient(view_.get());
+
+ // Arrange for the view to be synchronously destroyed inside
+ // SetVirtualKeyboardVisibilityIfEnabled.
+ input_method_->set_on_set_virtual_keyboard_visibility_if_enabled(
+ base::BindLambdaForTesting([&]() {
+ widget_host_ = nullptr;
+ view_.ExtractAsDangling()->Destroy();
+ }));
+
+ ui::mojom::TextInputState state;
+ state.type = ui::TEXT_INPUT_TYPE_TEXT;
+ state.mode = ui::TEXT_INPUT_MODE_TEXT;
+ state.show_ime_if_needed = true;
+ GetTextInputManager(view_.get())->UpdateTextInputState(view_.get(), state);
+}
+
// RWHVA::OnWindowFocused()'s lost-focus branch calls
// DetachFromInputMethod(false). On Windows this reaches
// TSFBridge::RemoveFocusedClient() -> ITfThreadMgr::AssociateFocus(), a