CVE-2025-11209
Overview
Background
- Location-bar-only mode ("mobar")
- an Android toolbar mode in which
ToolbarControlContainerhides the full toolbar and progress bar and reparents the location bar view (mLocationBarView) directly into the toolbar container. Leaving the mode reverses this. - `CoordinatorLayout`
- an Android
ViewGroupthat keeps its own processed list of children, refreshed only at measure time, and expects each child’sLayoutParamsto be its ownCoordinatorLayout.LayoutParamstype. - Reparenting with new `LayoutParams`
- When a view is removed from one parent and added to another, it gets that parent’s
LayoutParamstype (hereMarginLayoutParamsfromgetMarginLayoutParams()). Any code that still treats it as a child of the old parent will cast to the wrong type. - `ClassCastException`
- a Java runtime exception thrown when an object is cast to a type it is not; uncaught on the UI thread, it crashes the browser.
Root Cause Analysis
The vulnerable path is in ToolbarControlContainer.java, in the branch that leaves location-bar-only mode. The enclosing method’s signature is outside the hunk; the test drives it through toggleLocationBarOnlyMode(false). That branch calls mToolbarContainer.removeView(mLocationBarView) and then mToolbar.restoreLocationBarView(), which moves the location bar back into the toolbar with different LayoutParams (MarginLayoutParams, as the test sets up). As the added comment explains, CoordinatorLayout updates its processed list of children only at measure time. After removeView its cached list still contains the removed location bar, and the next layout pass can cast that view’s new MarginLayoutParams to CoordinatorLayout.LayoutParams, throwing a ClassCastException.
The fix calls forceLayout() and measure(...) on mToolbarContainer between removeView and restoreLocationBarView(), so the stale list is rebuilt before the view is reparented. The diff only shows how the crash is avoided. It does not show why the advisory classes this as an Omnibox “inappropriate implementation”. A crash or a broken toolbar state while the location bar is being restored could leave the omnibox missing or wrong, but that link is not shown in the diff (inferred).
CoordinatorLayout without letting the layout refresh its cached child list. A stale entry with incompatible LayoutParams could therefore be processed. The fix forces a re-measure right after removal so the cache is current before restoreLocationBarView() runs.Attack Path
- Enter location-bar-only mode
Chrome on Android switches the toolbar into mobar mode through
toggleLocationBarOnlyMode(true), which parentsmLocationBarViewintomToolbarContainer. The diff does not show what triggers this mode. - Leave the mode
On
toggleLocationBarOnlyMode(false), the container restores the toolbar and progress bar, removesmLocationBarView, and callsmToolbar.restoreLocationBarView(), which gives the view newMarginLayoutParams. - Stale child list in `CoordinatorLayout`
Before the fix, the container’s processed child list still referred to the reparented location bar, so a later measure or layout cast its params to the wrong type and threw a
ClassCastException. - Omnibox left broken or browser crashes (inferred) The uncaught exception crashes the browser, or interrupts the location-bar restore and leaves the omnibox state wrong. The security consequence the advisory has in mind is not shown in the diff.
Impact Assessment
ClassCastException in Chrome’s Android browser UI, meaning a crash (denial of service) in the browser process’s UI layer. Any URL-display or spoofing effect on the omnibox is (inferred). The bug affects only Chrome on Android, and only when the location-bar-only (mobar) toolbar mode is entered and then left. Whether ordinary web content can force those transitions, and whether the mode is behind a feature flag, cannot be determined from the diff.Files Changed
chrome/browser/ui/android/toolbar/java/src/org/chromium/chrome/browser/toolbar/top/ToolbarControlContainer.javachrome/browser/ui/android/toolbar/java/src/org/chromium/chrome/browser/toolbar/top/ToolbarControlContainerTest.java
Audit Directions
- View reparenting out of `CoordinatorLayout`Search the Android UI for
removeViewfollowed byaddViewinto another parent where the source is aCoordinatorLayout(or another layout that caches child lists) and no measure pass happens in between. - Security-critical UI restored in multiple stepsCheck other omnibox and toolbar mode toggles (location-bar-only mode, fullscreen and app-header states, desktop windowing) to make sure an exception or early exit partway through cannot leave the URL bar hidden, detached, or out of date.
- Unchecked `LayoutParams` castsReview custom layouts and behaviors that cast
getLayoutParams()without aninstanceofcheck, because any reparented child can turn those casts into a crash.
Patch
From cb2f937012403bbfa165cc067d2923223afcf86e Mon Sep 17 00:00:00 2001 From: Patrick Noland <[email protected]> Date: Thu, 28 Aug 2025 14:12:21 -0700 Subject: [PATCH] [mobar] Force re-measure of coordinator layout on leaving mobar This avoids a bug in CoordinatorLayout where a stale list of children can cause a ClassCastException after reparenting. Bug: 438226517 Change-Id: I2924109bfeb7fd8c855317a27dff730446b78a0b Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6896264 Reviewed-by: Tomasz Wiszkowski <[email protected]> Commit-Queue: Patrick Noland <[email protected]> Cr-Commit-Position: refs/heads/main@{#1508003} --- diff --git a/chrome/browser/ui/android/toolbar/java/src/org/chromium/chrome/browser/toolbar/top/ToolbarControlContainer.java b/chrome/browser/ui/android/toolbar/java/src/org/chromium/chrome/browser/toolbar/top/ToolbarControlContainer.java index b129393d..f81cc14b 100644 --- a/chrome/browser/ui/android/toolbar/java/src/org/chromium/chrome/browser/toolbar/top/ToolbarControlContainer.java +++ b/chrome/browser/ui/android/toolbar/java/src/org/chromium/chrome/browser/toolbar/top/ToolbarControlContainer.java @@ -241,6 +241,14 @@ mToolbar.getProgressBar().setVisibility(View.VISIBLE); mToolbarView.setVisibility(View.VISIBLE); mToolbarContainer.removeView(mLocationBarView); + // CoordinatorLayout only updates its processed list of children at measure time, even + // if a child is removed. This can cause problems if a reparented former child has a new + // type of LayoutParams, triggering a ClassCastException. We work around this by forcing + // a re-measure. + mToolbarContainer.forceLayout(); + mToolbarContainer.measure( + mToolbarContainer.getMeasuredWidthAndState(), + mToolbarContainer.getMeasuredHeightAndState()); mToolbar.restoreLocationBarView(); setBackgroundColor(Color.TRANSPARENT); } diff --git a/chrome/browser/ui/android/toolbar/java/src/org/chromium/chrome/browser/toolbar/top/ToolbarControlContainerTest.java b/chrome/browser/ui/android/toolbar/java/src/org/chromium/chrome/browser/toolbar/top/ToolbarControlContainerTest.java index c237447..502e57c59 100644 --- a/chrome/browser/ui/android/toolbar/java/src/org/chromium/chrome/browser/toolbar/top/ToolbarControlContainerTest.java +++ b/chrome/browser/ui/android/toolbar/java/src/org/chromium/chrome/browser/toolbar/top/ToolbarControlContainerTest.java @@ -22,6 +22,7 @@ import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; +import android.view.ViewGroup.MarginLayoutParams; import org.junit.Before; import org.junit.Rule; @@ -45,11 +46,21 @@ import org.chromium.chrome.browser.fullscreen.FullscreenManager; import org.chromium.chrome.browser.layouts.LayoutStateProvider; import org.chromium.chrome.browser.layouts.LayoutType; +import org.chromium.chrome.browser.omnibox.LocationBarCoordinator; +import org.chromium.chrome.browser.omnibox.LocationBarCoordinatorPhone; +import org.chromium.chrome.browser.omnibox.NewTabPageDelegate; import org.chromium.chrome.browser.tab.Tab; +import org.chromium.chrome.browser.tabmodel.IncognitoStateProvider; import org.chromium.chrome.browser.theme.SurfaceColorUpdateUtils; +import org.chromium.chrome.browser.theme.ThemeColorProvider; +import org.chromium.chrome.browser.toolbar.ToolbarDataProvider; import org.chromium.chrome.browser.toolbar.ToolbarFeatures; import org.chromium.chrome.browser.toolbar.ToolbarHairlineView; import org.chromium.chrome.browser.toolbar.ToolbarProgressBar; +import org.chromium.chrome.browser.toolbar.back_button.BackButtonCoordinator; +import org.chromium.chrome.browser.toolbar.forward_button.ForwardButtonCoordinator; +import org.chromium.chrome.browser.toolbar.menu_button.MenuButtonCoordinator; +import org.chromium.chrome.browser.toolbar.reload_button.ReloadButtonCoordinator; import org.chromium.chrome.browser.toolbar.top.CaptureReadinessResult.TopToolbarAllowCaptureReason; import org.chromium.chrome.browser.toolbar.top.CaptureReadinessResult.TopToolbarBlockCaptureReason; import org.chromium.chrome.browser.toolbar.top.ToolbarControlContainer.ToolbarViewResourceAdapter; @@ -57,7 +68,9 @@ import org.chromium.chrome.browser.toolbar.top.ToolbarControlContainer.ToolbarViewResourceCoordinatorLayout; import org.chromium.components.browser_ui.desktop_windowing.AppHeaderState; import org.chromium.components.browser_ui.widget.TouchEventObserver; +import org.chromium.components.embedder_support.util.UrlConstants; import org.chromium.ui.base.TestActivity; +import org.chromium.url.GURL; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.BooleanSupplier; @@ -79,11 +92,23 @@ @Mock private View mLocationBarView; @Mock private ToolbarHairlineView mToolbarHairline; @Mock private Toolbar mToolbar; + @Mock private ToggleTabStackButtonCoordinator mTabSwitcherButtonCoordinator; @Mock private ToolbarProgressBar mProgressBar; @Mock private Tab mTab; @Mock private LayoutStateProvider mLayoutStateProvider; @Mock private FullscreenManager mFullscreenManager; @Mock private TouchEventObserver mTouchEventObserver; + @Mock private LocationBarCoordinator mLocationBarCoordinator; + @Mock private LocationBarCoordinatorPhone mLocationBarCoordinatorPhone; + @Mock private MenuButtonCoordinator mMenuButtonCoordinator; + @Mock private ToolbarDataProvider mToolbarDataProvider; + @Mock private ReloadButtonCoordinator mReloadButtonCoordinator; + @Mock private BackButtonCoordinator mBackButtonCoordinator; + @Mock private ForwardButtonCoordinator mForwardButtonCoordinator; + @Mock private HomeButtonDisplay mHomeButtonDisplay; + @Mock private ThemeColorProvider mThemeColorProvider; + @Mock private IncognitoStateProvider mIncognitoStateProvider; + @Mock private NewTabPageDelegate mNewTabPageDelegate; private final Supplier<Tab> mTabSupplier = () -> mTab; private final ObservableSupplierImpl<Boolean> mCompositorInMotionSupplier = @@ -535,6 +560,28 @@ mLayoutStateProviderSupplier, mFullscreenManager); + ToolbarPhone toolbarPhone = controlContainer.findViewById(R.id.toolbar); + doReturn(mLocationBarCoordinatorPhone).when(mLocationBarCoordinator).getPhoneCoordinator(); + doReturn(mNewTabPageDelegate).when(mToolbarDataProvider).getNewTabPageDelegate(); + doReturn(new GURL(UrlConstants.ABOUT_URL)).when(mToolbarDataProvider).getCurrentGurl(); + toolbarPhone.setLocationBarCoordinator(mLocationBarCoordinator); + toolbarPhone.initialize( + mToolbarDataProvider, + null, + mMenuButtonCoordinator, + mTabSwitcherButtonCoordinator, + null, + null, + null, + mProgressBar, + mReloadButtonCoordinator, + mBackButtonCoordinator, + mForwardButtonCoordinator, + mHomeButtonDisplay, + /* extensionToolbarCoordinator= */ null, + mThemeColorProvider, + mIncognitoStateProvider); + controlContainer.toggleLocationBarOnlyMode(true); verify(mProgressBar).setVisibility(View.GONE); verify(mToolbarView).setVisibility(View.GONE); @@ -546,8 +593,9 @@ mLocationBarView, toolbarViewResourceFrameLayout.getChildAt( toolbarViewResourceFrameLayout.getChildCount() - 1)); - // check that location bar is parented to container view + MarginLayoutParams layoutParams = new MarginLayoutParams(500, 100); + doReturn(layoutParams).when(mLocationBarCoordinatorPhone).getMarginLayoutParams(); controlContainer.toggleLocationBarOnlyMode(false); verify(mProgressBar).setVisibility(View.VISIBLE); verify(mToolbarView).setVisibility(View.VISIBLE);