CVE-2026-102312
Overview
Background
- `UrlBar` scroll positioning
- the Android omnibox view computes and caches a horizontal scroll offset (
mPreviousScrollResultXPosition) so the URL is aligned to show the effective TLD/origin, and reuses the cached value when text and view metrics are unchanged. - `originChanged` signal
- a flag passed into the scroll request indicating the displayed URL’s origin changed (e.g., a cross-origin navigation), which must force a recompute rather than reuse of a cached scroll offset.
- Deferred (pending) scroll
- when layout is not ready (
isLayoutRequested()orgetLayout() == null), the scroll is postponed viamPendingScrolland later re-run fromlayout(), which carries no origin-change context of its own. - `mOriginEndIndex` / `mPreviousScrollOriginEndIndex`
- the current index at which the origin ends versus the index the cached scroll position was computed for; equality is required for a cached position to be valid.
Root Cause Analysis
In the scroll-request path, when mPendingScroll became true the method returned early without preserving originChanged, so the later deferred scroll run from layout() had no knowledge that the origin had changed. When the deferred scroll executed, the reuse condition (matching scroll type, font size, RTL, and isVisibleTextTheSame(text)) could be satisfied and the code would scrollTo(mPreviousScrollResultXPosition, 0), applying the scroll offset computed for the previous origin to the new one. This violated the invariant that a cached scroll offset is only valid for the origin it was computed for, letting the URL bar keep the previous origin’s alignment across a navigation to a different origin.
The fix latches the dropped signal into a new field mPendingScrollOriginChanged on the deferred path and ORs it back into originChanged (then clears it) when the scroll actually runs, and it also clears the latch on focus. As a defense-in-depth for callers lacking origin-change context, the reuse condition additionally requires mOriginEndIndex == mPreviousScrollOriginEndIndex, so a position computed for an origin ending at a different index is never reused.
originChanged signal when a scroll had to be deferred, so the deferred run reused a scroll offset computed for the prior origin; the fix latches the signal in mPendingScrollOriginChanged and additionally gates cache reuse on mOriginEndIndex == mPreviousScrollOriginEndIndex.Attack Path
- Prime a cached scroll for origin A
A page causes the URL bar to display and compute a scroll offset for an initial origin (e.g., a short domain), caching
mPreviousScrollResultXPositionandmPreviousScrollOriginEndIndex. - Navigate to origin B while layout is pending
Trigger a cross-origin navigation whose text change requests a new layout pass (e.g., a
wrap_contentview), so the scroll request is deferred withmPendingScroll = trueand, pre-patch,originChangedis dropped. - Deferred scroll reuses stale offset
When
layout()re-runs the scroll, the origin-change signal is absent and the reuse condition holds, so the URL bar applies origin A’s scroll offset to origin B’s text viascrollTo(...). - Outcome: origin misrepresentation The omnibox shows the new origin scrolled/aligned as the previous one, misleading the user about the site they are on (URL spoofing); no memory-safety primitive is involved.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/UrlBar.java |
modified |
Files Changed
chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/UrlBar.javachrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/UrlBarUnitTest.java
Audit Directions
- Signals dropped across deferral/async boundariesLook for state (like
originChanged) computed at request time but not carried into a deferred/posted continuation that later makes a security- or presentation-relevant decision. - Cache-validity keys that omit an identity fieldAudit position/metric caches whose reuse condition checks appearance (visible text, size, RTL) but not the identity the value was computed for (here, origin end index).
- Omnibox/URL presentation across navigationsReview other places where scroll offset, elision, or highlight state can survive a cross-origin navigation and misrepresent the displayed origin.
Patch
From 82b00091f852d33b3d6c7e16721136d5ba4b3590 Mon Sep 17 00:00:00 2001
From: Yoshisato Yanagisawa <yyanagisawa@chromium.org>
Date: Fri, 18 Sep 2026 12:13:51 -0700
Subject: [PATCH] Preserve origin changed state for deferred URL bar scroll requests
UrlBar reuses previously computed scroll positions when the current
text extends visible text and view metrics remain unchanged. When a
scroll update is deferred waiting for a pending layout pass, the
signal indicating an origin change was dropped because the deferred
invocation does not retain origin change information. As a result, the
URL bar could retain the previous scroll offset across navigations to
a different origin.
Latch the origin change signal for deferred scroll requests so it is
preserved and consumed when the deferred scroll runs. Additionally,
require the origin end index to match the index of the cached position
before reusing it, ensuring callers without origin change context do
not apply a scroll offset computed for a different origin.
Fixed: 551668264
Change-Id: I0c97c8ae43eb54fdf9a6f8c28fe5a6ba87f5af1d
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8423533
Auto-Submit: Tomasz Wiszkowski <ender@google.com>
Reviewed-by: Sky Malice <skym@chromium.org>
Commit-Queue: Tomasz Wiszkowski <ender@google.com>
Cr-Commit-Position: refs/heads/main@{#1701008}
---
diff --git a/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/UrlBar.java b/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/UrlBar.java
index e571254..ec38c02 100644
--- a/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/UrlBar.java
+++ b/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/UrlBar.java
@@ -151,6 +151,11 @@
private boolean mPendingScroll;
+ // Whether an origin change was reported for a scroll request that had to be deferred until
+ // the next layout pass. The deferred scroll performed from layout() has no origin change
+ // information of its own, so the signal is latched here and consumed when the scroll runs.
+ private boolean mPendingScrollOriginChanged;
+
// Captures the current intended text scroll type.
// This may not be effective if mPendingScroll is true.
@ScrollType private int mCurrentScrollType;
@@ -474,6 +479,7 @@
if (focused) {
mPendingScroll = false;
+ mPendingScrollOriginChanged = false;
}
fixupTextDirection();
@@ -1308,7 +1314,12 @@
// Request scroll update in case scroll type or view dimensions have changed.
mCurrentScrollType = scrollType;
mPendingScroll = isLayoutRequested() || (getLayout() == null);
- if (mPendingScroll) return;
+ if (mPendingScroll) {
+ mPendingScrollOriginChanged |= originChanged;
+ return;
+ }
+ originChanged |= mPendingScrollOriginChanged;
+ mPendingScrollOriginChanged = false;
if (mFocused) return;
@@ -1337,6 +1348,9 @@
// therefore false negative using regular equality is unlikely.
&& currentTextSize == mPreviousScrollFontSize
&& currentIsRtl == mPreviousScrollWasRtl
+ // A previously computed scroll position is only valid for text whose origin ends
+ // at the same index the position was computed for.
+ && mOriginEndIndex == mPreviousScrollOriginEndIndex
&& isVisibleTextTheSame(text)) {
scrollTo(mPreviousScrollResultXPosition, 0);
diff --git a/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/UrlBarUnitTest.java b/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/UrlBarUnitTest.java
index b4ac034a7..d15cecc 100644
--- a/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/UrlBarUnitTest.java
+++ b/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/UrlBarUnitTest.java
@@ -977,9 +977,7 @@
assertEquals(0, mUrlBar.getSelectionEnd());
}
- @Test
- public void scrollWhenOriginChanges() {
- // Initialize the URL bar. Verify test conditions.
+ private void setupInitialDomainScroll() {
mUrlBar.setText(SHORT_DOMAIN);
mUrlBar.setScrollState(
UrlBar.ScrollType.SCROLL_TO_TLD,
@@ -989,6 +987,11 @@
assertFalse(mUrlBar.hasPendingDisplayTextScrollForTesting());
verify(mUrlBar).scrollToTLD();
mUrlBar.setVisibleTextPrefixHintForTesting(SHORT_DOMAIN);
+ }
+
+ @Test
+ public void scrollWhenOriginChanges() {
+ setupInitialDomainScroll();
mUrlBar.setText(SHORT_SUBDOMAIN);
mUrlBar.setScrollState(
@@ -999,6 +1002,42 @@
}
@Test
+ public void scrollWhenOriginChanges_scrollDeferredUntilLayout() {
+ setupInitialDomainScroll();
+
+ // Simulate a text change that requests a new layout pass (e.g. a wrap_content view),
+ // deferring the scroll request until the pass completes.
+ mUrlBar.setText(SHORT_SUBDOMAIN);
+ mUrlBar.requestLayout();
+ mUrlBar.setScrollState(
+ UrlBar.ScrollType.SCROLL_TO_TLD,
+ /* scrollToIndex= */ SHORT_SUBDOMAIN.length(),
+ /* originChanged= */ true);
+ assertTrue(mUrlBar.hasPendingDisplayTextScrollForTesting());
+ verify(mUrlBar).scrollToTLD();
+
+ // The deferred scroll must honor the origin change and recompute the scroll position
+ // rather than restore the position computed for the previous origin.
+ measureAndLayoutUrlBar();
+ assertFalse(mUrlBar.hasPendingDisplayTextScrollForTesting());
+ verify(mUrlBar, times(2)).scrollToTLD();
+ }
+
+ @Test
+ public void scrollWhenOriginEndIndexChanges() {
+ setupInitialDomainScroll();
+
+ // Even without the origin change signal, a scroll position computed for an origin that
+ // ended at a different index must not be reused.
+ mUrlBar.setText(SHORT_SUBDOMAIN);
+ mUrlBar.setScrollState(
+ UrlBar.ScrollType.SCROLL_TO_TLD,
+ /* scrollToIndex= */ SHORT_SUBDOMAIN.length(),
+ /* originChanged= */ false);
+ verify(mUrlBar, times(2)).scrollToTLD();
+ }
+
+ @Test
@EnableFeatures(ChromeFeatureList.ANDROID_NO_VISIBLE_HINT_FOR_DIFFERENT_TLD)
public void scrollToTLD_sameTLD_calculateVisibleHint() {
doReturn(mLayout).when(mUrlBar).getLayout();