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 <[email protected]> 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 <[email protected]> Commit-Queue: Dave Tapuska <[email protected]> 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