Chrome · Core
CVE-2026-110918
Memory Corruption in Core
Overview
High
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifcontent/browser/renderer_host/text_input_manager.cc |
modified | |
ReentrantUpdateObservercontent/browser/renderer_host/text_input_manager_unittest.cc |
modified | |
ifcontent/browser/renderer_host/text_input_manager_unittest.cc |
modified | |
TextInputManagerReentrancyTestcontent/browser/renderer_host/text_input_manager_unittest.cc |
modified | |
BindLambdaForTestingcontent/browser/renderer_host/text_input_manager_unittest.cc |
modified | |
TEST_Fcontent/browser/renderer_host/text_input_manager_unittest.cc |
modified |
Files Changed
content/browser/renderer_host/text_input_manager.cccontent/browser/renderer_host/text_input_manager.hcontent/browser/renderer_host/text_input_manager_unittest.cc
Patch
From 1ecc753ba08893dfa91cfa98b3dbea6439fe0dde Mon Sep 17 00:00:00 2001 From: Jonathan Ross <[email protected]> Date: Wed, 07 Oct 2026 14:28:51 -0700 Subject: [PATCH] Guard TextInputManager against re-entrant destruction during notify TextInputManager::UpdateTextInputState() notifies observers in the middle of updating its state. When a newly focused view replaces the active view, it first notifies observers that the previously active view lost its text input state. RenderWidgetHostViewAura forwards this to InputMethod::OnTextInputTypeChanged(), where third-party IMEs can run a nested message loop. Tasks run there, such as window.close(), can destroy the WebContents, which owns the TextInputManager, or destroy the newly focused view. UpdateTextInputState() then continued with `active_view_ = view` on the freed TextInputManager, or went on to use the freed view. 77d3b67459722 made RenderWidgetHostViewAura robust to destruction during these IME callouts, but its caller here was not. Add a WeakPtrFactory to TextInputManager, and after each notification in UpdateTextInputState() return early if `this` has been destroyed or `view` is no longer registered. This also covers the final notification, after which `active_view_` was read without checking that `this` was still alive. Bug: 568937999 Change-Id: I751acf24c8a4c426bd7198d478333eed2ebf3f87 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8519361 Commit-Queue: Jonathan Ross <[email protected]> Reviewed-by: Aman Verma <[email protected]> Cr-Commit-Position: refs/heads/main@{#1713881} --- diff --git a/content/browser/renderer_host/text_input_manager.cc b/content/browser/renderer_host/text_input_manager.cc index 7dfe73f..449766f7a 100644 --- a/content/browser/renderer_host/text_input_manager.cc +++ b/content/browser/renderer_host/text_input_manager.cc @@ -313,6 +313,13 @@ new_edit_context_region != view_state.edit_context_selection_region; view_state.edit_context_selection_region = std::move(new_edit_context_region); + // Observers can run nested message loops (e.g. a third-party IME pumping + // messages inside InputMethod::OnTextInputTypeChanged()). Those can destroy + // the WebContents, and with it |this|, or unregister |view|. Re-validate + // both after every notification below before touching any state. + base::WeakPtr<TextInputManager> weak_this = weak_ptr_factory_.GetWeakPtr(); + base::WeakPtr<RenderWidgetHostViewBase> weak_view = view->GetWeakPtr(); + // If |view| is different from |active_view| and its |TextInputState.type| is // not NONE, |active_view_| should change to |view| only if |view| has focus. if (text_input_state.type != ui::TEXT_INPUT_TYPE_NONE && @@ -331,6 +338,11 @@ RenderWidgetHostViewBase* active_view = active_view_; active_view_ = nullptr; NotifyObserversAboutInputStateUpdate(active_view, true); + // |view| may also have been unregistered without being destroyed (e.g. + // moved to another TextInputManager); it must not become |active_view_|. + if (!weak_this || !weak_view || !IsRegistered(view)) { + return; + } } active_view_ = view; } @@ -343,9 +355,11 @@ active_view_ = nullptr; } - base::WeakPtr<RenderWidgetHostViewBase> weak_view = view->GetWeakPtr(); NotifyObserversAboutInputStateUpdate(view, changed); - if (edit_context_bounds_changed && weak_view && active_view_ == view) { + if (!weak_this || !weak_view) { + return; + } + if (edit_context_bounds_changed && active_view_ == view) { NotifySelectionBoundsChanged(view); } } diff --git a/content/browser/renderer_host/text_input_manager.h b/content/browser/renderer_host/text_input_manager.h index b7d56b1..352238b 100644 --- a/content/browser/renderer_host/text_input_manager.h +++ b/content/browser/renderer_host/text_input_manager.h @@ -12,6 +12,7 @@ #include "base/i18n/rtl.h" #include "base/memory/raw_ptr.h" +#include "base/memory/weak_ptr.h" #include "base/observer_list.h" #include "content/common/content_export.h" #include "third_party/blink/public/mojom/page/widget.mojom-forward.h" @@ -348,6 +349,8 @@ // TextInputManager::Observer reentrantly issues further notifications upon // `OnUpdateTextInputStateCalled()` (e.g. `SelectionBoundsChange()`). base::ReentrantObserverList<Observer>::Unchecked observer_list_; + + base::WeakPtrFactory<TextInputManager> weak_ptr_factory_{this}; }; } diff --git a/content/browser/renderer_host/text_input_manager_unittest.cc b/content/browser/renderer_host/text_input_manager_unittest.cc index edfb0ecc..991be42 100644 --- a/content/browser/renderer_host/text_input_manager_unittest.cc +++ b/content/browser/renderer_host/text_input_manager_unittest.cc @@ -5,7 +5,11 @@ #include "content/browser/renderer_host/text_input_manager.h" #include "base/command_line.h" +#include "base/functional/callback.h" +#include "base/test/bind.h" #include "build/build_config.h" +#include "content/browser/renderer_host/frame_tree.h" +#include "content/browser/renderer_host/frame_tree_node.h" #include "content/browser/renderer_host/render_frame_host_impl.h" #include "content/browser/renderer_host/render_widget_host_factory.h" #include "content/browser/renderer_host/render_widget_host_impl.h" @@ -453,4 +457,118 @@ manager->RemoveObserver(&observer); } +// Runs |callback| the first time an observer is told about an update for +// |target_view|, simulating a nested message loop (e.g. a third-party IME) +// running arbitrary tasks from inside the notification. +class ReentrantUpdateObserver : public TextInputManager::Observer { + public: + ReentrantUpdateObserver(RenderWidgetHostViewBase* target_view, + base::OnceClosure callback) + : target_view_(target_view), callback_(std::move(callback)) {} + + void OnUpdateTextInputStateCalled(TextInputManager* text_input_manager, + RenderWidgetHostViewBase* updated_view, + bool did_update_state) override { + if (updated_view != target_view_ || !callback_) { + return; + } + text_input_manager->RemoveObserver(this); + std::move(callback_).Run(); + } + + private: + raw_ptr<RenderWidgetHostViewBase, DisableDanglingPtrDetection> target_view_; + base::OnceClosure callback_; +}; + +class TextInputManagerReentrancyTest : public TextInputManagerTest { + protected: + // Commits a cross-site child frame, and makes the main frame the active + // text input view. + void SetUpMainAndChild() { + NavigationSimulator::CreateRendererInitiated(GURL("https://a.test/"), + main_rfh()) + ->Commit(); + child_rfh_ = static_cast<RenderFrameHostImpl*>( + NavigationSimulator::NavigateAndCommitFromDocument( + GURL("https://b.test/"), + RenderFrameHostTester::For(main_rfh())->AppendChild("child"))); + main_view_ = static_cast<RenderWidgetHostViewBase*>( + main_rfh()->GetRenderWidgetHost()->GetView()); + child_view_ = static_cast<RenderWidgetHostViewBase*>( + child_rfh_->GetRenderWidgetHost()->GetView()); + manager_ = main_view_->GetTextInputManager(); + ASSERT_TRUE(manager_); + ASSERT_EQ(manager_, child_view_->GetTextInputManager()); + + ui::mojom::TextInputState state; + state.type = ui::TEXT_INPUT_TYPE_TEXT; + manager_->UpdateTextInputState(main_view_, state); + ASSERT_EQ(main_view_, manager_->active_view_for_testing()); + + // Move focus to the child frame, so that its text input update replaces + // the main frame as the active view. + child_rfh_->frame_tree_node()->frame_tree().SetFocusedFrame( + child_rfh_->frame_tree_node(), nullptr); + } + + void UpdateChildTextInputState() { + ui::mojom::TextInputState state; + state.type = ui::TEXT_INPUT_TYPE_TEXT; + manager_->UpdateTextInputState(child_view_, state); + } + + raw_ptr<RenderFrameHostImpl, DisableDanglingPtrDetection> child_rfh_ = + nullptr; + raw_ptr<RenderWidgetHostViewBase, DisableDanglingPtrDetection> main_view_ = + nullptr; + raw_ptr<RenderWidgetHostViewBase, DisableDanglingPtrDetection> child_view_ = + nullptr; + raw_ptr<TextInputManager, DisableDanglingPtrDetection> manager_ = nullptr; +}; + +// Regression test for crbug.com/568937999. When a newly focused view replaces +// the active view, observers are notified that the previously active view lost +// its text input state. An observer that runs a nested message loop can close +// the tab, destroying the WebContents and its TextInputManager. +// UpdateTextInputState() must not touch the destroyed TextInputManager after +// that notification returns. +TEST_F(TextInputManagerReentrancyTest, + WebContentsDestroyedDuringActiveViewReset) { + ASSERT_NO_FATAL_FAILURE(SetUpMainAndChild()); + + bool contents_deleted = false; + ReentrantUpdateObserver observer(main_view_, + base::BindLambdaForTesting([&]() { + DeleteContents(); + contents_deleted = true; + })); + manager_->AddObserver(&observer); + + UpdateChildTextInputState(); + EXPECT_TRUE(contents_deleted); +} + +// Same as above, but the nested message loop only unregisters the newly +// focused child view without destroying it (e.g. the view moving to another +// TextInputManager). The TextInputManager survives, and the still-alive, +// focused view must not become the active view of a TextInputManager it is no +// longer registered with. +TEST_F(TextInputManagerReentrancyTest, ViewUnregisteredDuringActiveViewReset) { + ASSERT_NO_FATAL_FAILURE(SetUpMainAndChild()); + + bool child_unregistered = false; + ReentrantUpdateObserver observer(main_view_, + base::BindLambdaForTesting([&]() { + manager_->Unregister(child_view_); + child_unregistered = true; + })); + manager_->AddObserver(&observer); + + UpdateChildTextInputState(); + EXPECT_TRUE(child_unregistered); + EXPECT_FALSE(manager_->IsRegistered(child_view_)); + EXPECT_EQ(nullptr, manager_->active_view_for_testing()); +} + } // namespace content
Loading diff…
Regression Test / PoC
shipped with the fix
diff --git a/content/browser/renderer_host/text_input_manager_unittest.cc b/content/browser/renderer_host/text_input_manager_unittest.cc
index edfb0ecc..991be42 100644
--- a/content/browser/renderer_host/text_input_manager_unittest.cc
+++ b/content/browser/renderer_host/text_input_manager_unittest.cc
@@ -5,7 +5,11 @@
#include "content/browser/renderer_host/text_input_manager.h"
#include "base/command_line.h"
+#include "base/functional/callback.h"
+#include "base/test/bind.h"
#include "build/build_config.h"
+#include "content/browser/renderer_host/frame_tree.h"
+#include "content/browser/renderer_host/frame_tree_node.h"
#include "content/browser/renderer_host/render_frame_host_impl.h"
#include "content/browser/renderer_host/render_widget_host_factory.h"
#include "content/browser/renderer_host/render_widget_host_impl.h"
@@ -453,4 +457,118 @@
manager->RemoveObserver(&observer);
}
+// Runs |callback| the first time an observer is told about an update for
+// |target_view|, simulating a nested message loop (e.g. a third-party IME)
+// running arbitrary tasks from inside the notification.
+class ReentrantUpdateObserver : public TextInputManager::Observer {
+ public:
+ ReentrantUpdateObserver(RenderWidgetHostViewBase* target_view,
+ base::OnceClosure callback)
+ : target_view_(target_view), callback_(std::move(callback)) {}
+
+ void OnUpdateTextInputStateCalled(TextInputManager* text_input_manager,
+ RenderWidgetHostViewBase* updated_view,
+ bool did_update_state) override {
+ if (updated_view != target_view_ || !callback_) {
+ return;
+ }
+ text_input_manager->RemoveObserver(this);
+ std::move(callback_).Run();
+ }
+
+ private:
+ raw_ptr<RenderWidgetHostViewBase, DisableDanglingPtrDetection> target_view_;
+ base::OnceClosure callback_;
+};
+
+class TextInputManagerReentrancyTest : public TextInputManagerTest {
+ protected:
+ // Commits a cross-site child frame, and makes the main frame the active
+ // text input view.
+ void SetUpMainAndChild() {
+ NavigationSimulator::CreateRendererInitiated(GURL("https://a.test/"),
+ main_rfh())
+ ->Commit();
+ child_rfh_ = static_cast<RenderFrameHostImpl*>(
+ NavigationSimulator::NavigateAndCommitFromDocument(
+ GURL("https://b.test/"),
+ RenderFrameHostTester::For(main_rfh())->AppendChild("child")));
+ main_view_ = static_cast<RenderWidgetHostViewBase*>(
+ main_rfh()->GetRenderWidgetHost()->GetView());
+ child_view_ = static_cast<RenderWidgetHostViewBase*>(
+ child_rfh_->GetRenderWidgetHost()->GetView());
+ manager_ = main_view_->GetTextInputManager();
+ ASSERT_TRUE(manager_);
+ ASSERT_EQ(manager_, child_view_->GetTextInputManager());
+
+ ui::mojom::TextInputState state;
+ state.type = ui::TEXT_INPUT_TYPE_TEXT;
+ manager_->UpdateTextInputState(main_view_, state);
+ ASSERT_EQ(main_view_, manager_->active_view_for_testing());
+
+ // Move focus to the child frame, so that its text input update replaces
+ // the main frame as the active view.
+ child_rfh_->frame_tree_node()->frame_tree().SetFocusedFrame(
+ child_rfh_->frame_tree_node(), nullptr);
+ }
+
+ void UpdateChildTextInputState() {
+ ui::mojom::TextInputState state;
+ state.type = ui::TEXT_INPUT_TYPE_TEXT;
+ manager_->UpdateTextInputState(child_view_, state);
+ }
+
+ raw_ptr<RenderFrameHostImpl, DisableDanglingPtrDetection> child_rfh_ =
+ nullptr;
+ raw_ptr<RenderWidgetHostViewBase, DisableDanglingPtrDetection> main_view_ =
+ nullptr;
+ raw_ptr<RenderWidgetHostViewBase, DisableDanglingPtrDetection> child_view_ =
+ nullptr;
+ raw_ptr<TextInputManager, DisableDanglingPtrDetection> manager_ = nullptr;
+};
+
+// Regression test for crbug.com/568937999. When a newly focused view replaces
+// the active view, observers are notified that the previously active view lost
+// its text input state. An observer that runs a nested message loop can close
+// the tab, destroying the WebContents and its TextInputManager.
+// UpdateTextInputState() must not touch the destroyed TextInputManager after
+// that notification returns.
+TEST_F(TextInputManagerReentrancyTest,
+ WebContentsDestroyedDuringActiveViewReset) {
+ ASSERT_NO_FATAL_FAILURE(SetUpMainAndChild());
+
+ bool contents_deleted = false;
+ ReentrantUpdateObserver observer(main_view_,
+ base::BindLambdaForTesting([&]() {
+ DeleteContents();
+ contents_deleted = true;
+ }));
+ manager_->AddObserver(&observer);
+
+ UpdateChildTextInputState();
+ EXPECT_TRUE(contents_deleted);
+}
+
+// Same as above, but the nested message loop only unregisters the newly
+// focused child view without destroying it (e.g. the view moving to another
+// TextInputManager). The TextInputManager survives, and the still-alive,
+// focused view must not become the active view of a TextInputManager it is no
+// longer registered with.
+TEST_F(TextInputManagerReentrancyTest, ViewUnregisteredDuringActiveViewReset) {
+ ASSERT_NO_FATAL_FAILURE(SetUpMainAndChild());
+
+ bool child_unregistered = false;
+ ReentrantUpdateObserver observer(main_view_,
+ base::BindLambdaForTesting([&]() {
+ manager_->Unregister(child_view_);
+ child_unregistered = true;
+ }));
+ manager_->AddObserver(&observer);
+
+ UpdateChildTextInputState();
+ EXPECT_TRUE(child_unregistered);
+ EXPECT_FALSE(manager_->IsRegistered(child_view_));
+ EXPECT_EQ(nullptr, manager_->active_view_for_testing());
+}
+
} // namespace content
Loading diff…
Original Bug Report
The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.
References
On This Page