diff --git a/ui/android/overscroll_refresh_unittest.cc b/ui/android/overscroll_refresh_unittest.cc index 1234567890..abcdef0123 100644 --- a/ui/android/overscroll_refresh_unittest.cc +++ b/ui/android/overscroll_refresh_unittest.cc @@ -588,4 +588,56 @@ TEST_F(OverscrollRefreshTest, OverscrollBehaviorXNonePreventsTriggerStart) { TestOverscrollBehavior(ob, gfx::Vector2dF(10, 0), false); } +// POC: Demonstrates that viewport_width_ is read uninitialized when a +// horizontal overscroll arrives before the first OnFrameUpdated() call. +// +// In production this corresponds to a compromised renderer that: +// (1) withholds its first compositor frame so the browser never receives +// RenderFrameMetadata (the only writer of viewport_width_ via +// OverscrollControllerAndroid::OnFrameMetadataUpdated), and +// (2) sends WidgetInputHandlerHost::DidOverscroll with an x-dominant +// accumulated_overscroll after the user swipes the blank tab. +// +// The object is heap-allocated here (via make_unique) to match the production +// allocation site at content/browser/android/overscroll_controller_android.cc +// so that the 4 bytes backing viewport_width_ are raw heap garbage rather than +// zero-filled stack. +// +// Under MSAN this test fails with use-of-uninitialized-value at +// ui/android/overscroll_refresh.cc:109-110. Under a release build the +// scroll_from_edge result is non-deterministic and depends on the prior +// occupant of the PartitionAlloc slot. +TEST_F(OverscrollRefreshTest, POC_UninitializedViewportWidthRead) { + // Heap-allocate to match production (overscroll_controller_android.cc:76). + auto effect = std::make_unique(this, kDefaultEdgeWidth); + + // *** Deliberately skip OnFrameUpdated() *** + // In production, OnFrameUpdated is only called from + // RenderWidgetHostViewAndroid::OnRenderFrameMetadataChangedBeforeActivation, + // which a compromised renderer can prevent by withholding its first frame. + + // Step 1: Browser-side gesture detector emits kGestureScrollBegin for the + // user's swipe → OverscrollRefresh::OnScrollBegin. Use an x coordinate + // outside the left-edge band so the result depends solely on the + // uninitialized viewport_width_ term. + gfx::PointF mid_screen(kDefaultEdgeWidth + 50.f, 100.f); + effect->OnScrollBegin(mid_screen); + ASSERT_TRUE(effect->IsAwaitingScrollUpdateAck()); + + // Step 2: Compromised renderer sends DidOverscroll with x-dominant + // accumulated_overscroll and overscroll_behavior.x = kAuto. + // Reaches the in_x_direction branch and reads viewport_width_ at + // overscroll_refresh.cc:109-110. + gfx::Vector2dF x_dominant_overscroll(-1000.f, 0.f); + effect->OnOverscrolled(cc::OverscrollBehavior(), // x = y = kAuto + x_dominant_overscroll, + blink::WebGestureDevice::kTouchscreen); + + // The value of effect->IsActive() now depends on uninitialized heap bytes. + // MSAN: fatal use-of-uninitialized-value above. + // Release: non-deterministic; we record it but make no assertion on it. + bool leaked_bit = effect->IsActive(); + RecordProperty("leaked_bit_from_uninit_heap", leaked_bit ? "1" : "0"); +} + } // namespace ui