diff --git a/chrome/browser/ui/views/tabs/dragging/tab_drag_controller_interactive_uitest.cc b/chrome/browser/ui/views/tabs/dragging/tab_drag_controller_interactive_uitest.cc index 52a8d1aa4c43c..99b30a2432ceb 100644 --- a/chrome/browser/ui/views/tabs/dragging/tab_drag_controller_interactive_uitest.cc +++ b/chrome/browser/ui/views/tabs/dragging/tab_drag_controller_interactive_uitest.cc @@ -111,7 +111,11 @@ #include "ui/aura/client/aura_constants.h" #include "ui/aura/test/test_window_delegate.h" #include "ui/aura/test/test_windows.h" +#include "ui/aura/window.h" #include "ui/aura/window_targeter.h" +#include "ui/events/base_event_utils.h" +#include "ui/events/gestures/gesture_recognizer.h" +#include "ui/events/gestures/gesture_types.h" #endif #if defined(USE_AURA) && !BUILDFLAG(IS_CHROMEOS) @@ -815,6 +819,183 @@ IN_PROC_BROWSER_TEST_F(TabDragControllerTest, GestureEndShouldEndDragTest) { EXPECT_FALSE(IsDragSessionActive(tab_strip)); } +#if defined(USE_AURA) +namespace { + +// WindowFinder that always returns a fixed window so the test can control +// which tab strip GetDragTargetForPoint() resolves to. +class FixedWindowFinder : public WindowFinder { + public: + gfx::NativeWindow GetLocalProcessWindowAtPoint( + const gfx::Point& screen_point, + const std::set& ignore) override { + return window_; + } + void set_window(gfx::NativeWindow window) { window_ = window; } + + private: + gfx::NativeWindow window_; +}; + +// A second-touch gesture consumer plus a dispatch helper that, when asked to +// dispatch the synthetic kTouchCancelled generated by +// GestureRecognizerImpl::TransferEventsTo() -> +// CancelActiveTouchesExceptImpl(), reentrantly ends the active tab drag. +// This models the production scenario described at the hardened sibling site +// (TabDragController::DetachIntoNewBrowserAndRunMoveLoop), where the +// `attached_context_` receives a gesture-end during TransferEventsTo and +// destroys the controller. +class ReentrantDragEnder : public ui::GestureEventHelper { + public: + explicit ReentrantDragEnder(TabStrip* attached_tab_strip) + : consumer_(/*delegate=*/nullptr), + attached_tab_strip_(attached_tab_strip) { + // The consumer must be a real aura::Window because + // WindowEventDispatcher::CanDispatchToConsumer() unconditionally + // static_casts every GestureConsumer to aura::Window. Leave it without a + // root so that no real WindowEventDispatcher claims it; only this helper + // will. + consumer_.Init(ui::LAYER_NOT_DRAWN); + } + ~ReentrantDragEnder() override { + aura::Env::GetInstance()->gesture_recognizer()->RemoveGestureEventHelper( + this); + aura::Env::GetInstance()->gesture_recognizer()->CleanupStateForConsumer( + &consumer_); + } + + // Registers a consumer with one active pointer in the global + // GestureRecognizer, and this object as the dispatch helper for that + // consumer. After this, CancelActiveTouchesExceptImpl(anything-else) will + // route a synthetic touch-cancel through DispatchSyntheticTouchEvent() below. + void Arm() { + ui::GestureRecognizer* gr = + aura::Env::GetInstance()->gesture_recognizer(); + gr->AddGestureEventHelper(this); + ui::TouchEvent press( + ui::EventType::kTouchPressed, gfx::Point(5, 5), ui::EventTimeForNow(), + ui::PointerDetails(ui::EventPointerType::kTouch, /*pointer_id=*/5)); + gr->ProcessTouchEventPreDispatch(&press, &consumer_); + } + + bool fired() const { return fired_; } + + // ui::GestureEventHelper: + base::WeakPtr GetWeakPtr() override { + return weak_factory_.GetWeakPtr(); + } + bool CanDispatchToConsumer(ui::GestureConsumer* consumer) override { + return consumer == &consumer_; + } + void DispatchGestureEvent(ui::GestureConsumer* raw_input_consumer, + ui::GestureEvent* event) override {} + void DispatchSyntheticTouchEvent(ui::TouchEvent* event) override { + // We are now on the stack inside + // TabDragController::DragBrowserToNewTabStrip() -> + // GestureRecognizerImpl::TransferEventsTo() -> + // CancelActiveTouchesExceptImpl(). Reentrantly deliver a kGestureEnd to + // the attached tab strip's drag context, which calls + // TabDragController::EndDrag(kComplete) -> EndDragImpl() -> + // DestroyDragController(), freeing the TabDragController whose `this` + // pointer is still live up the stack. + fired_ = true; + ui::GestureEvent gesture_end( + 0, 0, 0, base::TimeTicks(), + ui::GestureEventDetails(ui::EventType::kGestureEnd)); + attached_tab_strip_->GetDragContext()->OnGestureEvent(&gesture_end); + } + + private: + aura::Window consumer_; + raw_ptr attached_tab_strip_; + bool fired_ = false; + base::WeakPtrFactory weak_factory_{this}; +}; + +} // namespace + +// Reproduces the unguarded-TransferEventsTo path in +// TabDragController::DragBrowserToNewTabStrip(): a second active-touch +// consumer's synthetic-cancel handler reentrantly ends the drag, freeing +// `this`, after which DragBrowserToNewTabStrip continues to read freed member +// state. The structurally identical call in DetachIntoNewBrowserAndRunMoveLoop +// is wrapped in `auto ref = weak_factory_.GetWeakPtr(); ... CHECK(ref)`; this +// site is not. +IN_PROC_BROWSER_TEST_F(TabDragControllerTest, + TransferEventsToReentrantDestructionInNewTabStrip) { + AddTabsAndResetBrowser(browser(), 1); + TabStrip* tab_strip = GetTabStripForBrowser(browser()); + + Browser* browser2 = CreateAnotherBrowserAndResize(); + TabStrip* tab_strip2 = GetTabStripForBrowser(browser2); + ASSERT_TRUE(tab_strip2); + + // Start a touch-sourced drag on the active tab. + browser()->tab_strip_model()->ActivateTabAt(1); + Tab* tab1 = tab_strip->tab_at(1); + gfx::Point tab1_center(tab1->width() / 2, tab1->height() / 2); + ui::GestureEvent gesture_tap_down( + tab1_center.x(), tab1_center.y(), 0, base::TimeTicks(), + ui::GestureEventDetails(ui::EventType::kGestureTapDown)); + tab_strip->MaybeStartDrag(tab1, gesture_tap_down, + tab_strip->GetSelectionModel()); + ASSERT_TRUE(TabDragController::IsActive()); + + // Force GetLocalProcessWindowAtPoint() to resolve to whichever browser we + // choose, independent of OS-level window stacking. + auto window_finder = std::make_unique(); + FixedWindowFinder* finder = window_finder.get(); + finder->set_window( + tab_strip->GetWidget()->GetTopLevelWidget()->GetNativeWindow()); + SetWindowFinderForTabStrip(tab_strip, std::move(window_finder)); + + TabDragContext* drag_context = tab_strip->GetDragContext(); + + // First scroll-update: stay inside the source strip so the controller + // transitions kNotStarted -> kDraggingTabs without detaching. + { + gfx::Point p = + ui_test_utils::GetCenterInScreenCoordinates(tab_strip->tab_at(0)); + views::View::ConvertPointFromScreen(drag_context, &p); + ui::GestureEvent scroll( + p.x(), p.y(), 0, base::TimeTicks(), + ui::GestureEventDetails(ui::EventType::kGestureScrollUpdate)); + HandleGestureEvent(tab_strip, &scroll); + } + ASSERT_TRUE(TabDragController::IsActive()); + ASSERT_TRUE(HasDragStarted(tab_strip)); + + // Arm the second-touch consumer *after* SetCapture(A), since SetCapture + // calls CancelActiveTouchesExcept() which would otherwise consume it early. + ReentrantDragEnder ender(tab_strip); + ender.Arm(); + + // Second scroll-update: target the second browser's tab strip so + // ContinueDragging() enters DragBrowserToNewTabStrip(target_context=B,...) + // and reaches the unguarded TransferEventsTo() call. + finder->set_window( + tab_strip2->GetWidget()->GetTopLevelWidget()->GetNativeWindow()); + { + gfx::Point p = + ui_test_utils::GetCenterInScreenCoordinates(tab_strip2->tab_at(0)); + views::View::ConvertPointFromScreen(drag_context, &p); + ui::GestureEvent scroll( + p.x(), p.y(), 0, base::TimeTicks(), + ui::GestureEventDetails(ui::EventType::kGestureScrollUpdate)); + // Under ASAN this dispatch reports heap-use-after-free on + // `this->current_state_` inside DragBrowserToNewTabStrip after + // TransferEventsTo() returns. Without sanitizers it would silently read + // freed memory and return Liveness::kAlive to the caller. + HandleGestureEvent(tab_strip, &scroll); + } + + EXPECT_TRUE(ender.fired()); + // If we got here, the controller survived; the unconditional kAlive return + // means the caller can't tell either way. + EXPECT_FALSE(TabDragController::IsActive()); +} +#endif // defined(USE_AURA) + class DetachToBrowserTabDragControllerTest : public TabDragControllerInteractiveTestMixin, public ::testing::WithParamInterface> {