CVE-2026-0906
Overview
Background
- `BrowserControlsManager`
- the Android Chrome class that positions and shows/hides the browser controls (toolbar/URL bar container,
mControlContainer) as the page scrolls or enters fullscreen. - `BrowserControlsState`
- the constraint enum (
SHOWN,HIDDEN,BOTH) that says whether the controls must be locked visible, locked hidden, or may move freely. - Constraint sources
- constraints can change from the browser side via
mBrowserVisibilityDelegate(aBrowserStateBrowserControlsVisibilityDelegate) or from the tab side, e.g. when the page focuses a form field and the tab requestsSHOWN; the tab path reaches the manager through a tab controls observer (onOffsetTagsInfoChangedin the test). - `mForceRelayoutOnVisibilityChange`
- a flag that makes the next visibility update force a
requestLayout()on the control container so a visibility change actually gets redrawn instead of leaving the toolbar hidden.
Root Cause Analysis
Before the patch, the logic that reacts to a SHOWN/BOTH constraint — calling setPositionsForTabToNonFullscreen() (unless a render-driven show is possible), calling scheduleVisibilityUpdate(), and, when getAndroidControlsVisibility() != View.VISIBLE, setting mForceRelayoutOnVisibilityChange = true — lived only inside an anonymous lambda registered as an observer on mBrowserVisibilityDelegate. The commit message states this “only runs for browserdelegate-driven constraint changes, which misses e.g. form-field focus driven SHOWN”: when the constraint came from the tab, the tab-observer path only called notifyConstraintsChanged(...) for offset tags and never ran the show/relayout logic.
As a result, a tab-driven request to lock the controls visible could leave the Android control container (which hosts the URL bar / security indicator) hidden or not redrawn, violating the invariant that a SHOWN constraint results in visible browser controls.
The fix extracts the lambda body verbatim into a new private method onConstraintsChanged(int), registers it as the delegate observer via this::onConstraintsChanged, and additionally calls onConstraintsChanged(constraints) from the tab-driven offset-tags path before notifying observers (now renamed notifyOffsetTagsChanged). The new unit test testConstraintChangeFromTab confirms that a tab-originated SHOWN with hidden, bottom-positioned controls now triggers requestLayout() on the container.
SHOWN → force the controls visible and relayout” handling was wired to only one of the two constraint sources (the browser visibility delegate), so tab-driven constraint changes skipped it. The fix centralises it in onConstraintsChanged and invokes it from both the delegate observer and the tab offset-tags path.Attack Path
- Page hides controls Web content on Chrome for Android scrolls or otherwise causes the browser controls (and with them the URL bar) to be hidden or moved off-screen, a normal state in the tested scenario (controls bottom-positioned and hidden).
- Tab-driven `SHOWN` constraint
The page triggers a tab-side constraint change such as focusing a form field, which the commit message names as a
SHOWNsource that bypassed the delegate observer. - Show/relayout logic skipped
Pre-patch, the tab path only notified offset-tag observers;
setPositionsForTabToNonFullscreen(),scheduleVisibilityUpdate(), and the forced relayout were never run, so the control container could stay hidden or un-redrawn. - Misleading UI state While the browser believes controls are locked shown, the user may not see the URL bar/security indicator, which an attacker could use to make page content appear to be browser UI or obscure the true origin (inferred).
Impact Assessment
SHOWN constraint (e.g. form-field focus) while controls are hidden, possibly with a user interaction to focus the field; exact exploitation as an origin-spoofing vector is (inferred). The vendor rates this as low severity (“Incorrect security UI”); the advisory provided does not state in-the-wild exploitation.Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchrome/android/java/src/org/chromium/chrome/browser/fullscreen/BrowserControlsManager.java |
modified |
Files Changed
chrome/android/java/src/org/chromium/chrome/browser/fullscreen/BrowserControlsManager.javachrome/android/junit/src/org/chromium/chrome/browser/fullscreen/BrowserControlsManagerUnitTest.java
Audit Directions
- Split event sources for security UILook for other browser-controls or omnibox visibility handlers in
chrome/android/.../fullscreen/andbrowser_controls/that are subscribed to only one of several constraint/visibility producers (delegate, tab, fullscreen API, keyboard, offset tags). - State recorded but not renderedAudit places where a visibility/constraint value is updated but the actual
Viewvisibility or layout is deferred (e.g.postOnAnimation, touch-in-progress delays), checking that every path eventually forces a redraw. - Page-controllable UI togglesReview page-triggerable transitions (fullscreen exit, form focus, virtual keyboard, scroll-driven hiding) for cases where the URL bar can remain hidden or stale while page content occupies its area.
Patch
From d9cb337ff4a744969259fa8cac73512f4f5fe2a4 Mon Sep 17 00:00:00 2001 From: Patrick Noland <[email protected]> Date: Thu, 11 Dec 2025 15:20:05 -0800 Subject: [PATCH] Force relayout for tab-driven constraint changes This logic currently only runs for browserdelegate-driven constraint changes, which misses e.g. form-field focus driven SHOWN. Bug: 467448811 Change-Id: I5662353216c8faea84a98e2f3edfa584cd6adf2c Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7253201 Reviewed-by: Wenyu Fu <[email protected]> Commit-Queue: Patrick Noland <[email protected]> Cr-Commit-Position: refs/heads/main@{#1557729} --- diff --git a/chrome/android/java/src/org/chromium/chrome/browser/fullscreen/BrowserControlsManager.java b/chrome/android/java/src/org/chromium/chrome/browser/fullscreen/BrowserControlsManager.java index 1f6a93c..9a78efa6 100644 --- a/chrome/android/java/src/org/chromium/chrome/browser/fullscreen/BrowserControlsManager.java +++ b/chrome/android/java/src/org/chromium/chrome/browser/fullscreen/BrowserControlsManager.java @@ -222,47 +222,7 @@ mBrowserVisibilityDelegate = new BrowserStateBrowserControlsVisibilityDelegate( mHtmlApiHandler.getPersistentFullscreenModeSupplier()); - mBrowserVisibilityDelegate.addObserver( - (constraints) -> { - if (constraints == BrowserControlsState.SHOWN) { - // When compositor can drive the animation to show controls, do not call - // setPositionsForTabToNonFullscreen to avoid control offset being forced - // set to 0 before the render-driven animation kicks in. - boolean allowRenderDrivenShowConstraint = - ChromeFeatureList.sBrowserControlsRenderDrivenShowConstraint - .isEnabled(); - boolean renderDrivenShowConstraint = - allowRenderDrivenShowConstraint - && canAnimateNativeBrowserControls(); - if (!renderDrivenShowConstraint) { - setPositionsForTabToNonFullscreen(); - } - - // TODO(https://crbug.com/449011189): Maybe cleanup - if (allowRenderDrivenShowConstraint) { - RecordHistogram.recordBooleanHistogram( - "Android.BrowserControls.RenderDrivenShowConstraint", - renderDrivenShowConstraint); - } - - // If controls become locked, it's possible we've previously delayed - // actually setting visibility until a touch event is over. In this case, we - // need to trigger an update again now, which should go through due to - // constraints. - scheduleVisibilityUpdate(); - } - - // From https://crbug.com/452885338, https://crbug.com/461532432: When changing - // controls visibility when exiting fullscreen, the visibility change might not - // honor a redraw. We do this through forcing a relayout to avoid the toolbar - // remains hidden. - if ((constraints == BrowserControlsState.SHOWN - || constraints == BrowserControlsState.BOTH) - && getAndroidControlsVisibility() != View.VISIBLE) { - mForceRelayoutOnVisibilityChange = true; - scheduleVisibilityUpdate(); - } - }); + mBrowserVisibilityDelegate.addObserver(this::onConstraintsChanged); } /** @@ -354,9 +314,10 @@ new OffsetTagConstraints( 0, 0, -(mTopControlsHeight + hairlineHeight), 0); + onConstraintsChanged(constraints); // Notify observers of changes before passing tags to native so observers // can set their relevant fields in offsetTagsInfo. - notifyConstraintsChanged(oldOffsetTagsInfo, offsetTagsInfo, constraints); + notifyOffsetTagsChanged(oldOffsetTagsInfo, offsetTagsInfo, constraints); offsetTagsInfo .getConstraints() @@ -850,7 +811,9 @@ return; } final int desiredVisibility = shouldShowAndroidControls() ? View.VISIBLE : View.INVISIBLE; - if (mControlContainer.getView().getVisibility() == desiredVisibility) return; + if (mControlContainer.getView().getVisibility() == desiredVisibility) { + return; + } mControlContainer.getView().removeCallbacks(mUpdateVisibilityRunnable); mControlContainer.getView().postOnAnimation(mUpdateVisibilityRunnable); } @@ -1022,7 +985,45 @@ } } - private void notifyConstraintsChanged( + private void onConstraintsChanged(@BrowserControlsState int constraints) { + if (constraints == BrowserControlsState.SHOWN) { + // When compositor can drive the animation to show controls, do not call + // setPositionsForTabToNonFullscreen to avoid control offset being forced + // set to 0 before the render-driven animation kicks in. + boolean allowRenderDrivenShowConstraint = + ChromeFeatureList.sBrowserControlsRenderDrivenShowConstraint.isEnabled(); + boolean renderDrivenShowConstraint = + allowRenderDrivenShowConstraint && canAnimateNativeBrowserControls(); + if (!renderDrivenShowConstraint) { + setPositionsForTabToNonFullscreen(); + } + + // TODO(https://crbug.com/449011189): Maybe cleanup + if (allowRenderDrivenShowConstraint) { + RecordHistogram.recordBooleanHistogram( + "Android.BrowserControls.RenderDrivenShowConstraint", + renderDrivenShowConstraint); + } + + // If controls become locked, it's possible we've previously delayed + // actually setting visibility until a touch event is over. In this case, we + // need to trigger an update again now, which should go through due to + // constraints. + scheduleVisibilityUpdate(); + } + + // From https://crbug.com/452885338, https://crbug.com/461532432: When changing + // controls visibility when exiting fullscreen, the visibility change might not + // honor a redraw. We do this through forcing a relayout to avoid the toolbar + // remains hidden. + if ((constraints == BrowserControlsState.SHOWN || constraints == BrowserControlsState.BOTH) + && getAndroidControlsVisibility() != View.VISIBLE) { + mForceRelayoutOnVisibilityChange = true; + scheduleVisibilityUpdate(); + } + } + + private void notifyOffsetTagsChanged( BrowserControlsOffsetTagsInfo oldOffsetTagsInfo, BrowserControlsOffsetTagsInfo offsetTagsInfo, @BrowserControlsState int constraints) { diff --git a/chrome/android/junit/src/org/chromium/chrome/browser/fullscreen/BrowserControlsManagerUnitTest.java b/chrome/android/junit/src/org/chromium/chrome/browser/fullscreen/BrowserControlsManagerUnitTest.java index 60399a7..e1d8392 100644 --- a/chrome/android/junit/src/org/chromium/chrome/browser/fullscreen/BrowserControlsManagerUnitTest.java +++ b/chrome/android/junit/src/org/chromium/chrome/browser/fullscreen/BrowserControlsManagerUnitTest.java @@ -54,6 +54,7 @@ import org.chromium.cc.input.BrowserControlsState; import org.chromium.chrome.R; import org.chromium.chrome.browser.ActivityTabProvider; +import org.chromium.chrome.browser.browser_controls.BrowserControlsOffsetTagsInfo; import org.chromium.chrome.browser.browser_controls.BrowserControlsStateProvider; import org.chromium.chrome.browser.browser_controls.BrowserControlsStateProvider.ControlsPosition; import org.chromium.chrome.browser.browser_controls.BrowserStateBrowserControlsVisibilityDelegate; @@ -784,6 +785,28 @@ anyBoolean()); } + @Test + public void testConstraintChangeFromTab() { + remakeWithoutSpy(); + notifyAddTab(mTab); + mActivityTabProvider.setForTesting(mTab); + // Put the control container in a hidden state and bottom-positioned. + mBrowserControlsManager.setControlsPosition( + ControlsPosition.BOTTOM, 0, 0, 0, TOOLBAR_HEIGHT, 10, TOOLBAR_HEIGHT); + ShadowLooper.idleMainLooper(); + Mockito.clearInvocations(mContainerView); + // Locking the controls via the TabControlsObserver should check for forced relayout. + mBrowserControlsManager + .getTabControlsObserverForTesting() + .onOffsetTagsInfoChanged( + mTab, + new BrowserControlsOffsetTagsInfo(), + new BrowserControlsOffsetTagsInfo(), + BrowserControlsState.SHOWN); + ShadowLooper.idleMainLooper(); + verify(mContainerView).requestLayout(); + } + private void verifyUpdateOffsetTagDefinitions( OffsetTagConstraints top, OffsetTagConstraints content, OffsetTagConstraints bottom) { BrowserControlsOffsetTagConstraints expectedConstraints =
Original Bug Report
Mini bar not rendered when omnibox is hidden (similar to issue 461532432)
Steps to reproduce the problem
- Open the testcase.
- Scroll down to the middle of the page until the omnibox disappears (normal fullscreen scroll behavior).
- Tap inside the <textarea>.
Problem Description
This issue appears very similar to Chromium issue 461532432.
In Chrome Canary on Android, when the omnibox auto-hides during scroll, tapping inside a <textarea> causes the keyboard’s accessory bar (the mini bar above the virtual keyboard) to not render at all. The space where the accessory bar normally appears is still allocated, but it is completely empty (no icons, no background, just blank UI).
This empty UI zone appears exactly where a user expects browser controls. A website can use CSS to place a fake omnibox just below it, enabling UI spoofing.
Summary
Mini bar not rendered when omnibox is hidden (similar to issue 461532432)
Additional Data
Category: Security
Chrome Channel: Canary
Regression: N/A \