CVE-2026-79132
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifcontent/browser/android/overscroll_controller_android.cc |
modified | |
ifcontent/browser/renderer_host/render_widget_host_view_android.cc |
modified | |
TEST_Fui/android/overscroll_refresh_unittest.cc |
modified |
Files Changed
content/browser/android/overscroll_controller_android.cccontent/browser/android/overscroll_controller_android.hcontent/browser/renderer_host/render_widget_host_view_android.ccui/android/overscroll_refresh.ccui/android/overscroll_refresh.hui/android/overscroll_refresh_unittest.cc
Patch
From 8d0f86ea0f21884d501c0750a215d513fedb7bc4 Mon Sep 17 00:00:00 2001 From: Bo Liu <[email protected]> Date: Mon, 27 Jul 2026 12:23:21 -0700 Subject: [PATCH] android: Use trusted value for overscroll edge detection Scrollable viewport can be smaller than view size for things like browser controls or insets. But for edge scroll detection, it actually makes sense to just use the view size in these cases anyway. So mitigate a minor security issue. Bug: 513834155 Change-Id: Ie8254e2875e6a9caadb2077282418c7d56062bf7 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8141959 Reviewed-by: Kartar Singh <[email protected]> Commit-Queue: Bo Liu <[email protected]> Cr-Commit-Position: refs/heads/main@{#1668879} --- diff --git a/content/browser/android/overscroll_controller_android.cc b/content/browser/android/overscroll_controller_android.cc index 0a554bdf..08a2f75 100644 --- a/content/browser/android/overscroll_controller_android.cc +++ b/content/browser/android/overscroll_controller_android.cc @@ -254,6 +254,7 @@ } void OverscrollControllerAndroid::OnFrameMetadataUpdated( + float view_width_px, float page_scale_factor, float device_scale_factor, const gfx::SizeF& scrollable_viewport_size, @@ -273,8 +274,9 @@ gfx::ScalePoint(root_scroll_offset, scale_factor); if (refresh_effect_) { - refresh_effect_->OnFrameUpdated(viewport_size, content_scroll_offset, - content_size, root_overflow_y_hidden); + refresh_effect_->OnFrameUpdated(view_width_px, viewport_size.height(), + content_scroll_offset, content_size, + root_overflow_y_hidden); } if (glow_effect_) { diff --git a/content/browser/android/overscroll_controller_android.h b/content/browser/android/overscroll_controller_android.h index 3aaf999..af46de0 100644 --- a/content/browser/android/overscroll_controller_android.h +++ b/content/browser/android/overscroll_controller_android.h @@ -68,8 +68,10 @@ // Note: The effect will detach itself when no further animation is required. bool Animate(base::TimeTicks current_time, cc::slim::Layer* parent_layer); - // To be called whenever the content frame has been updated. - void OnFrameMetadataUpdated(float page_scale_factor, + // To be called whenever the content frame has been updated. `view_width_px` + // is the width of the embedding native view in device pixels. + void OnFrameMetadataUpdated(float view_width_px, + float page_scale_factor, float device_scale_factor, const gfx::SizeF& scrollable_viewport_size, const gfx::SizeF& root_layer_size, diff --git a/content/browser/renderer_host/render_widget_host_view_android.cc b/content/browser/renderer_host/render_widget_host_view_android.cc index dd3fe213..1b0b117 100644 --- a/content/browser/renderer_host/render_widget_host_view_android.cc +++ b/content/browser/renderer_host/render_widget_host_view_android.cc @@ -1003,8 +1003,9 @@ if (overscroll_controller_) { overscroll_controller_->OnFrameMetadataUpdated( - metadata.page_scale_factor, metadata.device_scale_factor, - metadata.scrollable_viewport_size, metadata.root_layer_size, + view_.GetSizeDevicePx().width(), metadata.page_scale_factor, + metadata.device_scale_factor, metadata.scrollable_viewport_size, + metadata.root_layer_size, metadata.root_scroll_offset.value_or(gfx::PointF()), metadata.root_overflow_y_hidden); } diff --git a/ui/android/overscroll_refresh.cc b/ui/android/overscroll_refresh.cc index 6e41d8d..3770f3e 100644 --- a/ui/android/overscroll_refresh.cc +++ b/ui/android/overscroll_refresh.cc @@ -115,6 +115,8 @@ } } else if (in_x_direction) { DCHECK_GE(viewport_width_, 0); + DCHECK_LE(scroll_begin_x_, viewport_width_); + DCHECK_GE(scroll_begin_x_, 0); bool scroll_from_edge = scroll_begin_x_ < edge_width_ || viewport_width_ - scroll_begin_x_ < edge_width_; bool touchpad_swipe_to_navigate = @@ -205,15 +207,17 @@ return scroll_state_.IsAwaitingAck(); } -void OverscrollRefresh::OnFrameUpdated(const gfx::SizeF& viewport_size, +void OverscrollRefresh::OnFrameUpdated(float view_width, + float scrollable_viewport_height, const gfx::PointF& content_scroll_offset, const gfx::SizeF& content_size, bool root_overflow_y_hidden) { - viewport_width_ = viewport_size.width(); + viewport_width_ = view_width; scrolled_to_top_ = content_scroll_offset.y() == 0; if (base::FeatureList::IsEnabled(kReportBottomOverscrolls)) { - scrolled_to_bottom_ = content_size.height() <= - content_scroll_offset.y() + viewport_size.height(); + scrolled_to_bottom_ = + content_size.height() <= + content_scroll_offset.y() + scrollable_viewport_height; } overflow_y_hidden_ = root_overflow_y_hidden; } diff --git a/ui/android/overscroll_refresh.h b/ui/android/overscroll_refresh.h index 184570a7..7ebfb24 100644 --- a/ui/android/overscroll_refresh.h +++ b/ui/android/overscroll_refresh.h @@ -96,8 +96,11 @@ // Notify the effect of the latest scroll offset and overflow properties. // The effect will be disabled when the offset is non-zero or overflow is - // hidden. Note: All dimensions are in device pixels. - void OnFrameUpdated(const gfx::SizeF& viewport_size, + // hidden. Note: All dimensions are in device pixels. `view_width` is the + // width of the embedding native view and is used for edge-swipe gating; + // `scrollable_viewport_height` reflects the page's scrollable viewport. + void OnFrameUpdated(float view_width, + float scrollable_viewport_height, const gfx::PointF& content_scroll_offset, const gfx::SizeF& content_size, bool root_overflow_y_hidden); diff --git a/ui/android/overscroll_refresh_unittest.cc b/ui/android/overscroll_refresh_unittest.cc index d2c659f..e903f28 100644 --- a/ui/android/overscroll_refresh_unittest.cc +++ b/ui/android/overscroll_refresh_unittest.cc @@ -17,6 +17,8 @@ const float kDefaultEdgeWidth = OverscrollRefresh::kDefaultNavigationEdgeWidth * kDipScale; const gfx::SizeF kViewport(100, 100); +const float kViewWidth = 100; +const float kViewportHeight = 100; const gfx::PointF kZeroOffset(0, 0); const gfx::SizeF kContentSize(100, 10000); const bool kOverflowYNotHidden = false; @@ -29,8 +31,8 @@ OverscrollRefreshTest() : OverscrollRefreshHandler(nullptr), effect_(OverscrollRefresh(this, kDefaultEdgeWidth)) { - effect_.OnFrameUpdated(kViewport, kZeroOffset, kContentSize, - kOverflowYNotHidden); + effect_.OnFrameUpdated(kViewWidth, kViewportHeight, kZeroOffset, + kContentSize, kOverflowYNotHidden); } // OverscrollRefreshHandler implementation. @@ -192,11 +194,11 @@ // A positive y scroll offset at the start of scroll will prevent activation, // even if the subsequent scroll overscrolls upward. gfx::PointF nonzero_offset(0, 10); - effect_.OnFrameUpdated(kViewport, nonzero_offset, kContentSize, - kOverflowYNotHidden); + effect_.OnFrameUpdated(kViewWidth, kViewportHeight, nonzero_offset, + kContentSize, kOverflowYNotHidden); effect_.OnScrollBegin(kStartPos); - effect_.OnFrameUpdated(kViewport, kZeroOffset, kContentSize, + effect_.OnFrameUpdated(kViewWidth, kViewportHeight, kZeroOffset, kContentSize, kOverflowYNotHidden); gfx::Vector2dF scroll_delta = gfx::Vector2dF(0, 10); ASSERT_FALSE(effect_.WillHandleScrollUpdate(scroll_delta)); @@ -214,7 +216,8 @@ TEST_F(OverscrollRefreshTest, RefreshNotTriggeredIfOverflowYHidden) { // overflow-y:hidden at the start of scroll will prevent activation. - effect_.OnFrameUpdated(kViewport, kZeroOffset, kContentSize, true); + effect_.OnFrameUpdated(kViewWidth, kViewportHeight, kZeroOffset, kContentSize, + true); effect_.OnScrollBegin(kStartPos); gfx::Vector2dF scroll_delta = gfx::Vector2dF(0, 10); @@ -234,7 +237,8 @@ TEST_F(OverscrollRefreshTest, RefreshNotTriggeredIfOverflowYHiddenNoUpdateBeforeOverscroll) { // overflow-y:hidden at the start of scroll will prevent activation. - effect_.OnFrameUpdated(kViewport, kZeroOffset, kContentSize, true); + effect_.OnFrameUpdated(kViewWidth, kViewportHeight, kZeroOffset, kContentSize, + true); effect_.OnScrollBegin(kStartPos); gfx::Vector2dF scroll_delta = gfx::Vector2dF(0, 10); @@ -350,8 +354,8 @@ // Set yOffset as reaching the bottom of the page. gfx::PointF nonzero_offset(0, 900); gfx::SizeF content_size(100, 1000); - effect_.OnFrameUpdated(kViewport, nonzero_offset, content_size, - kOverflowYNotHidden); + effect_.OnFrameUpdated(kViewWidth, kViewportHeight, nonzero_offset, + content_size, kOverflowYNotHidden); gfx::PointF start(2.f, 902.f); effect_.OnScrollBegin(start); @@ -397,11 +401,11 @@ // since it's not starting from the bottom, even if the subsequent scroll
Regression Test / PoC
diff --git a/ui/android/overscroll_refresh_unittest.cc b/ui/android/overscroll_refresh_unittest.cc
index d2c659f..e903f28 100644
--- a/ui/android/overscroll_refresh_unittest.cc
+++ b/ui/android/overscroll_refresh_unittest.cc
@@ -17,6 +17,8 @@
const float kDefaultEdgeWidth =
OverscrollRefresh::kDefaultNavigationEdgeWidth * kDipScale;
const gfx::SizeF kViewport(100, 100);
+const float kViewWidth = 100;
+const float kViewportHeight = 100;
const gfx::PointF kZeroOffset(0, 0);
const gfx::SizeF kContentSize(100, 10000);
const bool kOverflowYNotHidden = false;
@@ -29,8 +31,8 @@
OverscrollRefreshTest()
: OverscrollRefreshHandler(nullptr),
effect_(OverscrollRefresh(this, kDefaultEdgeWidth)) {
- effect_.OnFrameUpdated(kViewport, kZeroOffset, kContentSize,
- kOverflowYNotHidden);
+ effect_.OnFrameUpdated(kViewWidth, kViewportHeight, kZeroOffset,
+ kContentSize, kOverflowYNotHidden);
}
// OverscrollRefreshHandler implementation.
@@ -192,11 +194,11 @@
// A positive y scroll offset at the start of scroll will prevent activation,
// even if the subsequent scroll overscrolls upward.
gfx::PointF nonzero_offset(0, 10);
- effect_.OnFrameUpdated(kViewport, nonzero_offset, kContentSize,
- kOverflowYNotHidden);
+ effect_.OnFrameUpdated(kViewWidth, kViewportHeight, nonzero_offset,
+ kContentSize, kOverflowYNotHidden);
effect_.OnScrollBegin(kStartPos);
- effect_.OnFrameUpdated(kViewport, kZeroOffset, kContentSize,
+ effect_.OnFrameUpdated(kViewWidth, kViewportHeight, kZeroOffset, kContentSize,
kOverflowYNotHidden);
gfx::Vector2dF scroll_delta = gfx::Vector2dF(0, 10);
ASSERT_FALSE(effect_.WillHandleScrollUpdate(scroll_delta));
@@ -214,7 +216,8 @@
TEST_F(OverscrollRefreshTest, RefreshNotTriggeredIfOverflowYHidden) {
// overflow-y:hidden at the start of scroll will prevent activation.
- effect_.OnFrameUpdated(kViewport, kZeroOffset, kContentSize, true);
+ effect_.OnFrameUpdated(kViewWidth, kViewportHeight, kZeroOffset, kContentSize,
+ true);
effect_.OnScrollBegin(kStartPos);
gfx::Vector2dF scroll_delta = gfx::Vector2dF(0, 10);
@@ -234,7 +237,8 @@
TEST_F(OverscrollRefreshTest,
RefreshNotTriggeredIfOverflowYHiddenNoUpdateBeforeOverscroll) {
// overflow-y:hidden at the start of scroll will prevent activation.
- effect_.OnFrameUpdated(kViewport, kZeroOffset, kContentSize, true);
+ effect_.OnFrameUpdated(kViewWidth, kViewportHeight, kZeroOffset, kContentSize,
+ true);
effect_.OnScrollBegin(kStartPos);
gfx::Vector2dF scroll_delta = gfx::Vector2dF(0, 10);
@@ -350,8 +354,8 @@
// Set yOffset as reaching the bottom of the page.
gfx::PointF nonzero_offset(0, 900);
gfx::SizeF content_size(100, 1000);
- effect_.OnFrameUpdated(kViewport, nonzero_offset, content_size,
- kOverflowYNotHidden);
+ effect_.OnFrameUpdated(kViewWidth, kViewportHeight, nonzero_offset,
+ content_size, kOverflowYNotHidden);
gfx::PointF start(2.f, 902.f);
effect_.OnScrollBegin(start);
@@ -397,11 +401,11 @@
// since it's not starting from the bottom, even if the subsequent scroll
// overscrolls upward.
gfx::SizeF content_size(100, 110);
- effect_.OnFrameUpdated(kViewport, kZeroOffset, content_size,
+ effect_.OnFrameUpdated(kViewWidth, kViewportHeight, kZeroOffset, content_size,
kOverflowYNotHidden);
effect_.OnScrollBegin(kStartPos);
- effect_.OnFrameUpdated(kViewport, kZeroOffset, content_size,
+ effect_.OnFrameUpdated(kViewWidth, kViewportHeight, kZeroOffset, content_size,
kOverflowYNotHidden);
gfx::Vector2dF scroll_delta = gfx::Vector2dF(0, -10);
ASSERT_FALSE(effect_.WillHandleScrollUpdate(scroll_delta));
@@ -419,11 +423,11 @@
TEST_F(OverscrollRefreshTest, NotTriggeredIfContentSizeEqualsToViewport) {
// bottom overscroll only triggers when content is scrollable.
- effect_.OnFrameUpdated(kViewport, gfx::PointF(), kViewport,
+ effect_.OnFrameUpdated(kViewWidth, kViewportHeight, gfx::PointF(), kViewport,
kOverflowYNotHidden);
effect_.OnScrollBegin(kStartPos);
- effect_.OnFrameUpdated(kViewport, gfx::PointF(), kViewport,
+ effect_.OnFrameUpdated(kViewWidth, kViewportHeight, gfx::PointF(), kViewport,
kOverflowYNotHidden);
gfx::Vector2dF scroll_delta = gfx::Vector2dF(0, -10);
ASSERT_FALSE(effect_.WillHandleScrollUpdate(scroll_delta));
@@ -444,8 +448,8 @@
// Set yOffset as reaching the bottom of the page.
gfx::PointF nonzero_offset(0, 900);
gfx::SizeF content_size(100, 1000);
- effect_.OnFrameUpdated(kViewport, nonzero_offset, content_size,
- kOverflowYNotHidden);
+ effect_.OnFrameUpdated(kViewWidth, kViewportHeight, nonzero_offset,
+ content_size, kOverflowYNotHidden);
gfx::PointF start(2.f, 902.f);
effect_.OnScrollBegin(start);
@@ -469,8 +473,8 @@
// Set yOffset as reaching the bottom of the page.
gfx::PointF nonzero_offset(0, 900);
gfx::SizeF content_size(100, 1000);
- effect_.OnFrameUpdated(kViewport, nonzero_offset, content_size,
- kOverflowYNotHidden);
+ effect_.OnFrameUpdated(kViewWidth, kViewportHeight, nonzero_offset,
+ content_size, kOverflowYNotHidden);
gfx::PointF start(2.f, 902.f);
effect_.OnScrollBegin(start);
@@ -713,8 +717,8 @@
}
TEST_F(OverscrollRefreshTest, MultidimensionalOverscroll) {
- effect_.OnFrameUpdated(kViewport, gfx::PointF(0, 100), kContentSize,
- kOverflowYNotHidden);
+ effect_.OnFrameUpdated(kViewWidth, kViewportHeight, gfx::PointF(0, 100),
+ kContentSize, kOverflowYNotHidden);
effect_.OnScrollBegin(gfx::PointF(2.f, 50.f));
// First overscroll event: pure horizontal overscroll starts History Nav
Original Bug Report
Potential bypass of overscroll edge-swipe gating on Android via spoofed viewport size
Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: A compromised renderer can potentially bypass the physical edge-swipe requirement for history navigation gestures on Android by spoofing viewport dimensions in RenderFrameMetadata. By reporting a zero width, the browser’s internal logic for gating navigation transitions becomes trivially satisfied for any touch position. This allows an attacker to instantiate and interact with the BackForwardTransitionAnimator in the browser process using any scroll gesture.
Affected files:
ui/android/overscroll_refresh.cccontent/browser/android/overscroll_controller_android.cccc/mojom/render_frame_metadata_mojom_traits.cccontent/browser/renderer_host/render_widget_host_view_android.cccontent/browser/navigation_transitions/back_forward_transition_animation_manager_android.cc
Estimated timestamp from git blame: 2019-04-18
Description
A logic flaw in Chromium’s Android overscroll implementation allows a compromised renderer to bypass the security gating for history navigation transitions (edge-swipes). The browser process relies on renderer-supplied RenderFrameMetadata to determine the viewport width, which is subsequently used to verify if a horizontal scroll gesture originated near the physical edge of the screen.
Root Cause Analysis
In ui/android/overscroll_refresh.cc, the OnOverscrolled method evaluates if a gesture qualifies as a history navigation (kHistoryNavigation) using the following check:
// ui/android/overscroll_refresh.cc:109
bool scroll_from_edge = scroll_begin_x_ < edge_width_ ||
viewport_width_ - scroll_begin_x_ < edge_width_;
The viewport_width_ is updated via OnFrameUpdated, which receives its values from the renderer through OverscrollControllerAndroid::OnFrameMetadataUpdated. A compromised renderer can spoof the scrollable_viewport_size or page_scale_factor in the RenderFrameMetadata IPC such that the calculated viewport_width_ in the browser becomes zero.
When viewport_width_ is 0, the expression 0 - scroll_begin_x_ < edge_width_ simplifies to -scroll_begin_x_ < edge_width_. Since scroll_begin_x_ is a non-negative touch coordinate provided by the browser’s input system and edge_width_ is a positive constant (defaulting to 24px), this condition is always satisfied regardless of where the user touches the screen.
Potential Impact
By bypassing this gating, a compromised renderer can force the browser to instantiate the BackForwardTransitionAnimator. This is a complex browser-process component (approximately 2400 lines) that manages page snapshots, suppresses user input, and interacts with navigation lifecycle events.
While direct memory corruption has not been proven, this bypass significantly expands the browser-process attack surface available to a compromised renderer. It allows an attacker to trigger privileged UI transitions, suppress input for the tab, and probe for race conditions in the animator’s state machine against RenderFrameHost and navigation events. This feature is typically active on Android devices with >= 7.2GB RAM.
Potential Steps to Trigger
Note: These steps are theoretical as we have not executed code to verify a Proof of Concept.
- From a compromised renderer, ensure the tab has session history (e.g., via
history.pushState). - Send a
cc::mojom::RenderFrameMetadataObserverClient::OnRenderFrameMetadataChangedIPC withscrollable_viewport_sizeset to{0, 0}. - The browser updates
OverscrollRefresh::viewport_width_to 0. - Wait for the user to perform any scroll gesture on the page.
- The renderer sends a
blink::mojom::WidgetInputHandlerHost::DidOverscrollIPC indicating horizontal overscroll. - The browser’s edge check evaluates to true due to the zeroed width, triggering the
BackForwardTransitionAnimator.
Suggested Fix
Validate scrollable_viewport_size and page_scale_factor in the browser process within RenderWidgetHostViewAndroid::OnRenderFrameMetadataChangedBeforeActivation. Ensure that viewport_width_ in OverscrollRefresh is sanitized and maintains a sane minimum value before being used in navigation gating logic. Additionally, consider performing the edge-start validation earlier in the input pipeline where the viewport size is known from the browser’s perspective.
Evaluated with Chrome root at commit: 1a8d40fc44df2088d5945c0bf53584038aa1614a
Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:
- If you are familiar with the severity guidelines, you may adjust the severity.
- If this is a false positive, and there’s no work to be done, please close as WAI.
- If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.
Data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.