diff --git a/ui/views/focus/focus_manager_unittest.cc b/ui/views/focus/focus_manager_unittest.cc index a0af398e50633..c405a10f51877 100644 --- a/ui/views/focus/focus_manager_unittest.cc +++ b/ui/views/focus/focus_manager_unittest.cc @@ -199,6 +199,31 @@ class DestructiveView : public View { BEGIN_METADATA(DestructiveView) END_METADATA +// A View that synchronously deletes itself when it loses focus. This mirrors +// real-world UI patterns where transient elements (e.g. SystemTextfield active +// state callbacks) destroy themselves on blur. The framework explicitly +// supports this — View::Blur() and SetFocusedViewWithReason() both use +// ViewTracker to survive it. +class SelfDeletingOnBlurView : public View { + METADATA_HEADER(SelfDeletingOnBlurView, View) + public: + SelfDeletingOnBlurView() { + SetFocusBehavior(FocusBehavior::ALWAYS); + set_suppress_default_focus_handling(); + } + + void OnBlur() override { + // Transient UI elements may remove and destroy themselves when losing + // focus. SetFocusedViewWithReason() handles this via ViewTracker, but + // StoreFocusedView() caches the raw pointer on the stack before calling + // SetFocusedView(nullptr). + parent()->RemoveChildViewT(this); + } +}; + +BEGIN_METADATA(SelfDeletingOnBlurView) +END_METADATA + class DidChangeFocusListener : public FocusChangeListener { public: void OnWillChangeFocus(View* focused_before, View* focused_now) override {} @@ -244,6 +269,41 @@ TEST_F(FocusManagerTest, FocusChangeWithSynchronousDestruction) { RemoveFocusChangeListener(&listener); } +// Regression test: StoreFocusedView() caches the focused view in raw stack +// pointers, then calls SetFocusedView(nullptr) which invokes Blur() on the old +// view. If the view synchronously destroys itself during Blur() — a pattern +// the framework explicitly supports elsewhere — the cached pointers dangle and +// the subsequent SetStoredFocusView() / SchedulePaint() are use-after-free. +TEST_F(FocusManagerTest, StoreFocusedViewSurvivesSelfDeletingBlur) { + SelfDeletingOnBlurView* view = GetContentsView()->AddChildView( + std::make_unique()); + view->RequestFocus(); + ASSERT_EQ(view, GetFocusManager()->GetFocusedView()); + + // This is the production entry point hit on window deactivation + // (DesktopNativeWidgetAura passes clear_native_focus=false). It triggers + // OnBlur() -> self-deletion, then dereferences the freed view. + GetFocusManager()->StoreFocusedView(/*clear_native_focus=*/false); + + // If we survive without ASAN tripping, the stored view must not point at + // freed memory. + EXPECT_EQ(nullptr, GetFocusManager()->GetStoredFocusView()); +} + +// Same scenario for the clear_native_focus=true path used by NativeWidgetAura, +// which routes through ClearFocus() but still ends with v->SchedulePaint() on +// the cached raw pointer. +TEST_F(FocusManagerTest, StoreFocusedViewClearNativeSurvivesSelfDeletingBlur) { + SelfDeletingOnBlurView* view = GetContentsView()->AddChildView( + std::make_unique()); + view->RequestFocus(); + ASSERT_EQ(view, GetFocusManager()->GetFocusedView()); + + GetFocusManager()->StoreFocusedView(/*clear_native_focus=*/true); + + EXPECT_EQ(nullptr, GetFocusManager()->GetStoredFocusView()); +} + TEST_F(FocusManagerTest, NativeViewFocusChangeListener) { // First, ensure the simulator is aware of the Widget created in SetUp() being // currently active.