diff --git a/content/browser/renderer_host/input/input_router_impl_unittest.cc b/content/browser/renderer_host/input/input_router_impl_unittest.cc index c9bb9def7d55e..6b41f7567b544 100644 --- a/content/browser/renderer_host/input/input_router_impl_unittest.cc +++ b/content/browser/renderer_host/input/input_router_impl_unittest.cc @@ -2962,4 +2962,81 @@ TEST_F(InputRouterImplTest, SynchronousDestructionDuringAck) { EXPECT_EQ(nullptr, input_router_); } +// SendKeyboardEvent calls StopFling() before dispatching the key event. With a +// touchscreen fling armed, StopFling() synchronously emits a kGestureScrollEnd +// which is dispatched non-blocking and acked synchronously through +// disposition_handler_->OnGestureEventAck(). In production this reaches +// OverscrollController::CompleteAction() -> GestureNavSimple::GoBack() which +// can tear down the owning RenderWidgetHost (see the in-tree weak_this guards +// at OverscrollController::CompleteAction and the sibling +// InputRouterImpl::OnTouchEventAck). SendKeyboardEvent has no liveness guard +// after StopFling() returns and continues to read/call through |this|. +// +// This test models the embedder destruction by resetting |input_router_| from +// the gesture-ack callout. Under ASan it produces a heap-use-after-free with +// SendKeyboardEvent on the call stack and ~InputRouterImpl on the free stack. +TEST_F(InputRouterImplTest, SynchronousDestructionDuringSendKeyboardEvent) { + // Begin a touchscreen scroll so the fling controller will accept a fling. + PressAndSetTouchActionAuto(); + SimulateGestureEvent(WebInputEvent::Type::kGestureScrollBegin, + blink::WebGestureDevice::kTouchscreen); + SimulateGestureScrollUpdateEvent(10.f, 0.f, 0, + blink::WebGestureDevice::kTouchscreen); + + // Ack everything dispatched so far so the gesture ack queue is drained. + for (auto& msg : GetAndResetDispatchedMessages()) { + if (msg->ToEvent()) { + msg->ToEvent()->CallCallback( + blink::mojom::InputEventResultState::kConsumed); + } + } + + // Arm a touchscreen fling: GFS is consumed by the FlingController and sets + // |fling_curve_|. The GFS is acked synchronously to the disposition handler + // and never enters the gesture ack queue. + SimulateGestureFlingStartEvent(1000.f, 0.f, + blink::WebGestureDevice::kTouchscreen); + + // ProgressFling() may have emitted a momentum GSU; drain it too so that the + // synthetic GSE's synchronous ack is delivered immediately rather than + // queued behind a pending GSU. + for (auto& msg : GetAndResetDispatchedMessages()) { + if (msg->ToEvent()) { + msg->ToEvent()->CallCallback( + blink::mojom::InputEventResultState::kConsumed); + } + } + disposition_handler_->GetAndResetAckCount(); + + // Model embedder teardown (OverscrollController::CompleteAction -> GoBack -> + // ~RenderInputRouter -> input_router_.reset()) when the synthetic GSE is + // acked. In production this destruction occurs inside the + // disposition_handler_->OnGestureEventAck callout reached from StopFling(). + disposition_handler_->set_on_gesture_event_ack_closure(base::BindRepeating( + [](std::unique_ptr* router, + MockInputRouterClient* client, + MockInputDispositionHandler* handler) { + if (*router) { + client->set_input_router(nullptr); + handler->set_input_router(nullptr); + router->reset(); + } + }, + &input_router_, client_.get(), disposition_handler_.get())); + + // Keyboard input arrives while the fling is active. SendKeyboardEvent calls + // gesture_event_queue_.StopFling() at the top of the function, which + // synchronously re-enters the gesture pipeline, reaches OnGestureEventAck, + // and frees |this|. SendKeyboardEvent then continues to read |weak_this_| + // and call FilterAndSendWebInputEvent() on the freed object. + // + // The first ASan report fires at an inner frame of the same synchronous + // chain (the GestureEventQueue/FlingController members are also part of the + // freed allocation); SendKeyboardEvent is on the call stack and would + // perform further freed-|this| accesses after the inner frames return. + SimulateKeyboardEvent(WebInputEvent::Type::kRawKeyDown); + + EXPECT_EQ(nullptr, input_router_); +} + } // namespace content diff --git a/content/browser/renderer_host/input/mock_input_disposition_handler.cc b/content/browser/renderer_host/input/mock_input_disposition_handler.cc index 56c258891ccd4..ba8d71640af97 100644 --- a/content/browser/renderer_host/input/mock_input_disposition_handler.cc +++ b/content/browser/renderer_host/input/mock_input_disposition_handler.cc @@ -73,6 +73,9 @@ void MockInputDispositionHandler::OnGestureEventAck( VLOG(1) << __FUNCTION__ << " called!"; acked_gesture_event_ = event.event; RecordAckCalled(event.event.GetType(), ack_result); + if (on_gesture_event_ack_closure_) { + on_gesture_event_ack_closure_.Run(); + } } size_t MockInputDispositionHandler::GetAndResetAckCount() { diff --git a/content/browser/renderer_host/input/mock_input_disposition_handler.h b/content/browser/renderer_host/input/mock_input_disposition_handler.h index c2e55287d6395..35af1c5f899cb 100644 --- a/content/browser/renderer_host/input/mock_input_disposition_handler.h +++ b/content/browser/renderer_host/input/mock_input_disposition_handler.h @@ -57,6 +57,10 @@ class MockInputDispositionHandler : public input::InputDispositionHandler { on_touch_event_ack_closure_ = std::move(closure); } + void set_on_gesture_event_ack_closure(base::RepeatingClosure closure) { + on_gesture_event_ack_closure_ = std::move(closure); + } + blink::mojom::InputEventResultState ack_state() const { return ack_state_; } blink::mojom::InputEventResultState acked_wheel_event_state() const { @@ -105,6 +109,7 @@ class MockInputDispositionHandler : public input::InputDispositionHandler { std::unique_ptr gesture_followup_event_; std::unique_ptr touch_followup_event_; base::OnceClosure on_touch_event_ack_closure_; + base::RepeatingClosure on_gesture_event_ack_closure_; }; } // namespace content