Chrome · Messages
CVE-2026-113145
Memory Corruption in Messages
Overview
Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchrome/browser/ui/messages/android/java/src/org/chromium/chrome/browser/ui/messages/snackbar/SnackbarCollection.java |
modified | |
forchrome/browser/ui/messages/android/java/src/org/chromium/chrome/browser/ui/messages/snackbar/SnackbarCollectionUnitTest.java |
modified |
Files Changed
chrome/browser/ui/messages/android/java/src/org/chromium/chrome/browser/ui/messages/snackbar/SnackbarCollection.javachrome/browser/ui/messages/android/java/src/org/chromium/chrome/browser/ui/messages/snackbar/SnackbarCollectionUnitTest.java
Patch
From 10eb54ec0e4e22a8ad6539f912bd6538db863991 Mon Sep 17 00:00:00 2001 From: Vikram Pasupathy <[email protected]> Date: Wed, 23 Sep 2026 10:08:25 -0700 Subject: [PATCH] Suspend persistent high-priority snackbar when preempted by transient high-priority notice When a persistent high-priority snackbar (such as a TWA or WebAPK privacy disclosure) is showing and a transient action high-priority notice (such as an ExclusiveAccessBubble fullscreen notice) is triggered, SnackbarCollection previously dismissed the persistent disclosure immediately with REPLACED_BY_HIGH_PRIORITY. This allowed untrusted web content to suppress the privacy disclosure by entering fullscreen. This CL isolates high-priority snackbars from the standard queues by giving transient and persistent high-priority notices dedicated slots in SnackbarCollection (mActiveTransientHighPriority and mActivePersistentHighPriority). The transient notice takes precedence while active so its timeout runs without delay, while the persistent disclosure remains safely held and automatically resurfaces when the transient notice is dismissed. Additionally, normal action snackbars are prevented from dismissing an active persistent high-priority disclosure. Recording of bug: http://shortn/_b4chABIbP3 Recording of fix: http://shortn/_zulcmovEz1 Bug: 553169805 Change-Id: I7faaaf21db25af62d850a38ee4e6fc2f68cc3a4a Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8428677 Reviewed-by: Aishwarya Rajesh <[email protected]> Commit-Queue: Vikram Pasupathy <[email protected]> Cr-Commit-Position: refs/heads/main@{#1703545} --- diff --git a/chrome/browser/ui/messages/android/java/src/org/chromium/chrome/browser/ui/messages/snackbar/SnackbarCollection.java b/chrome/browser/ui/messages/android/java/src/org/chromium/chrome/browser/ui/messages/snackbar/SnackbarCollection.java index e7dc81e1..393ad80 100644 --- a/chrome/browser/ui/messages/android/java/src/org/chromium/chrome/browser/ui/messages/snackbar/SnackbarCollection.java +++ b/chrome/browser/ui/messages/android/java/src/org/chromium/chrome/browser/ui/messages/snackbar/SnackbarCollection.java @@ -28,9 +28,15 @@ private final Deque<Snackbar> mSnackbars = new ArrayDeque<>(); // Queue for persistent notifications (e.g., "Offline mode"). private final Deque<Snackbar> mPersistentSnackbars = new ArrayDeque<>(); - // Holds a single High Priority security warning (e.g., Fullscreen). - // This isolates HP snackbars from the standard queues, allowing O(1) interruption. - private @Nullable Snackbar mActiveHighPriority; + // TODO(crbug.com/564820082): Explore alternatives to handle preemption and resumption across + // different notice lifecycles (transient countdown vs. persistent state). + // Holds a transient High Priority security warning (e.g., Fullscreen ExclusiveAccessBubble). + // Takes precedence over persistent disclosures so its timeout can run immediately. + private @Nullable Snackbar mActiveTransientHighPriority; + // Holds a persistent High Priority disclosure (e.g., TWA / WebAPK "Running in Chrome"). + // Displayed whenever no transient HP warning is active, completely isolated from standard + // queues. + private @Nullable Snackbar mActivePersistentHighPriority; private static final int MAX_SNACKBARS = 10; @@ -39,39 +45,39 @@ "Snackbar.QueueDepthAtInsertion", mSnackbars.size() + mPersistentSnackbars.size() - + (mActiveHighPriority != null ? 1 : 0), + + (mActiveTransientHighPriority != null ? 1 : 0) + + (mActivePersistentHighPriority != null ? 1 : 0), MAX_SNACKBARS + 1); if (snackbar.isHighPriority()) { - // High Priority (HP) Security Warnings bypass the regular queues entirely. - if (mActiveHighPriority != null) { + Snackbar currentHpInSlot = + snackbar.isTypePersistent() + ? mActivePersistentHighPriority + : mActiveTransientHighPriority; + if (currentHpInSlot != null) { // State Deduplication: Rapid oscillation attacks (e.g., spamming requestFullscreen) // are collapsed here. If it's the exact same warning, we ignore the duplicate. - if (mActiveHighPriority.getController() == snackbar.getController() + if (currentHpInSlot.getController() == snackbar.getController() && Objects.equals( - mActiveHighPriority.getActionData(), snackbar.getActionData())) { + currentHpInSlot.getActionData(), snackbar.getActionData())) { // Defensive update: If a controller submits a new Snackbar instance with // modified content (e.g., updated text) instead of mutating in place, // update the active instance rather than dropping the update as a duplicate. - if (!mActiveHighPriority.equals(snackbar)) { - mActiveHighPriority = snackbar; + if (!currentHpInSlot.equals(snackbar)) { + setHighPrioritySlot(snackbar); } return; // Deduplication: already have this HP snackbar. } - // LIFO Immediate Interruption: The new HP warning instantly overwrites the old one. + // HP snackbars of the same rank replace each other. RecordHistogram.recordEnumeratedHistogram( "Snackbar.DismissalReason", DismissalReason.REPLACED_BY_HIGH_PRIORITY, DismissalReason.NUM_ENTRIES); - assert mActiveHighPriority.getController() != null; - if (mActiveHighPriority.getController() != null) { - mActiveHighPriority - .getController() - .onDismissNoAction(mActiveHighPriority.getActionData()); - } + assert currentHpInSlot.getController() != null; + currentHpInSlot.getController().onDismissNoAction(currentHpInSlot.getActionData()); } - // Assign to the VIP slot. getCurrent() will now prioritize this above all else. - mActiveHighPriority = snackbar; + // Assign to its dedicated HP rank slot without disturbing the other HP rank slot. + setHighPrioritySlot(snackbar); return; } @@ -80,7 +86,9 @@ : "Persistent snackbars require action text."; mPersistentSnackbars.addFirst(snackbar); } else if (snackbar.isTypeAction()) { - if (getCurrent() != null && !getCurrent().isTypeAction()) { + if (getCurrent() != null + && !getCurrent().isTypeAction() + && !getCurrent().isHighPriority()) { removeCurrent(DismissalReason.REPLACED_BY_ACTION_SNACKBAR); } mSnackbars.addFirst(snackbar); @@ -98,6 +106,14 @@ } } + private void setHighPrioritySlot(Snackbar snackbar) { + if (snackbar.isTypePersistent()) { + mActivePersistentHighPriority = snackbar; + } else { + mActiveTransientHighPriority = snackbar; + } + } + private void removeOldestUnseenSnackbar(Deque<Snackbar> list) { Iterator<Snackbar> iter = list.iterator(); if (iter.hasNext() && list.peekFirst() == getCurrent()) { @@ -115,13 +131,15 @@ } boolean contains(Snackbar snackbar) { - return snackbar == mActiveHighPriority + return snackbar == mActiveTransientHighPriority + || snackbar == mActivePersistentHighPriority || mSnackbars.contains(snackbar) || mPersistentSnackbars.contains(snackbar); } @Nullable Snackbar getCurrent() { - if (mActiveHighPriority != null) return mActiveHighPriority; + if (mActiveTransientHighPriority != null) return mActiveTransientHighPriority; + if (mActivePersistentHighPriority != null) return mActivePersistentHighPriority; Snackbar actionCurrent = mSnackbars.peekFirst(); Snackbar persistentCurrent = mPersistentSnackbars.peekFirst(); return actionCurrent != null ? actionCurrent : persistentCurrent; @@ -139,8 +157,10 @@ Snackbar current = getCurrent(); if (current == null) return null; - if (current == mActiveHighPriority) { - mActiveHighPriority = null; + if (current == mActiveTransientHighPriority) { + mActiveTransientHighPriority = null; + } else if (current == mActivePersistentHighPriority) { + mActivePersistentHighPriority = null; } else if (!mSnackbars.isEmpty() && mSnackbars.peekFirst() == current) { mSnackbars.pollFirst(); } else { @@ -161,7 +181,8 @@ } boolean isEmpty() { - return mActiveHighPriority == null + return mActiveTransientHighPriority == null + && mActivePersistentHighPriority == null && mSnackbars.isEmpty() && mPersistentSnackbars.isEmpty(); } @@ -180,16 +201,29 @@ removeCurrent(DismissalReason.TIMEOUT); } + private boolean dismissHighPriorityIfMatching( + @Nullable Snackbar hp, + SnackbarController controller, + @Nullable Object actionData, + boolean matchActionData) { + if (hp == null || hp.getController() != controller) return false; + if (matchActionData && !Objects.equals(hp.getActionData(), actionData)) return false; + RecordHistogram.recordEnumeratedHistogram( + "Snackbar.DismissalReason", + DismissalReason.DISMISSED_BY_CALLER, + DismissalReason.NUM_ENTRIES); + controller.onDismissNoAction(hp.getActionData()); + return true; + } + boolean removeMatchingSnackbars(SnackbarController controller) { boolean removed = false; - if (mActiveHighPriority != null && mActiveHighPriority.getController() == controller) { - Snackbar removedHp = mActiveHighPriority; - mActiveHighPriority = null; - RecordHistogram.recordEnumeratedHistogram( - "Snackbar.DismissalReason", - DismissalReason.DISMISSED_BY_CALLER, - DismissalReason.NUM_ENTRIES); - controller.onDismissNoAction(removedHp.getActionData()); + if (dismissHighPriorityIfMatching(mActiveTransientHighPriority, controller, null, false)) { + mActiveTransientHighPriority = null; + removed = true; + } + if (dismissHighPriorityIfMatching(mActivePersistentHighPriority, controller, null, false)) { + mActivePersistentHighPriority = null; removed = true; } removed |= @@ -222,16 +256,12 @@ boolean removeMatchingSnackbars(SnackbarController controller, Object data) { boolean removed = false; - if (mActiveHighPriority != null - && mActiveHighPriority.getController() == controller - && Objects.equals(mActiveHighPriority.getActionData(), data)) { - Snackbar removedHp = mActiveHighPriority; - mActiveHighPriority = null; - RecordHistogram.recordEnumeratedHistogram( - "Snackbar.DismissalReason", - DismissalReason.DISMISSED_BY_CALLER, - DismissalReason.NUM_ENTRIES); - controller.onDismissNoAction(assumeNonNull(removedHp.getActionData())); + if (dismissHighPriorityIfMatching(mActiveTransientHighPriority, controller, data, true)) { + mActiveTransientHighPriority = null; + removed = true; + } + if (dismissHighPriorityIfMatching(mActivePersistentHighPriority, controller, data, true)) { + mActivePersistentHighPriority = null; removed = true; } removed |= diff --git a/chrome/browser/ui/messages/android/java/src/org/chromium/chrome/browser/ui/messages/snackbar/SnackbarCollectionUnitTest.java b/chrome/browser/ui/messages/android/java/src/org/chromium/chrome/browser/ui/messages/snackbar/SnackbarCollectionUnitTest.java index 9e4feb0..4574387 100644 --- a/chrome/browser/ui/messages/android/java/src/org/chromium/chrome/browser/ui/messages/snackbar/SnackbarCollectionUnitTest.java +++ b/chrome/browser/ui/messages/android/java/src/org/chromium/chrome/browser/ui/messages/snackbar/SnackbarCollectionUnitTest.java @@ -29,7 +29,6 @@ import org.robolectric.shadows.ShadowLooper; import org.chromium.base.test.BaseRobolectricTestRunner; -import org.chromium.base.test.util.Feature; import org.chromium.base.test.util.HistogramWatcher; import org.chromium.chrome.browser.ui.messages.snackbar.SnackbarManager.DismissalReason; import org.chromium.chrome.browser.ui.messages.snackbar.SnackbarManager.SnackbarController; @@ -46,7 +45,6 @@ @Mock private SnackbarController mMockController; @Test - @Feature({"Browser", "Snackbar"}) public void testActionCoversNotification() { SnackbarCollection collection = new SnackbarCollection(); assertTrue(collection.isEmpty()); @@ -71,7 +69,6 @@ } @Test - @Feature({"Browser", "Snackbar"}) public void testNotificationGoesUnderAction() { SnackbarCollection collection = new SnackbarCollection(); assertTrue(collection.isEmpty()); @@ -101,7 +98,6 @@ } @Test - @Feature({"Browser", "Snackbar"}) public void testClear() { SnackbarCollection collection = new SnackbarCollection(); for (int i = 0; i < 3; i++) { @@ -115,7 +111,6 @@ } @Test - @Feature({"Browser", "Snackbar"}) public void testRemoveMatchingSnackbars() { SnackbarCollection collection = new SnackbarCollection(); for (int i = 0; i < 3; i++) { @@ -136,7 +131,6 @@ } @Test - @Feature({"Browser", "Snackbar"}) public void testRemoveMatchingSnackbarsWithData() { SnackbarCollection collection = new SnackbarCollection(); for (int i = 0; i < 3; i++) { @@ -163,7 +157,6 @@ // This test is added as a result of crbug.com/40796914. // Test that the action/dismiss callbacks are not invoked when the controller is null. @Test - @Feature({"Browser", "Snackbar"}) public void testRemoveCurrent_ControllerNotSpecified() { SnackbarCollection collection = new SnackbarCollection(); assertTrue(collection.isEmpty()); @@ -185,7 +178,6 @@ } @Test - @Feature({"Browser", "Snackbar"}) public void testDismissalReasons() { SnackbarCollection collection = new SnackbarCollection(); @@ -248,7 +240,6 @@ } @Test - @Feature({"Browser", "Snackbar"}) public void testHighPriorityActionSnackbarNotDiscardedOnTimeout() { SnackbarCollection collection = new SnackbarCollection(); assertTrue(collection.isEmpty()); @@ -280,7 +271,6 @@ } @Test - @Feature({"Browser", "Snackbar"}) public void testMultipleHighPrioritySnackbarsShownSequentially() { SnackbarCollection collection = new SnackbarCollection(); @@ -315,7 +305,6 @@ } @Test - @Feature({"Browser", "Snackbar", "Security"}) public void testQueueExhaustion_HighPriorityPreemptsSpam() { SnackbarCollection collection = new SnackbarCollection(); SnackbarController spamController = mock(SnackbarController.class); @@ -349,7 +338,6 @@ } @Test - @Feature({"Browser", "Snackbar", "Security"}) public void testQueueExhaustion_EvictsOldestUnseen() { SnackbarCollection collection = new SnackbarCollection(); SnackbarController[] controllers = new SnackbarController[15]; @@ -386,7 +374,6 @@ } @Test - @Feature({"Browser", "Snackbar", "Security"}) public void testOscillationAttack_StateDeduplication() { SnackbarCollection collection = new SnackbarCollection(); SnackbarController controller = mock(SnackbarController.class); @@ -414,7 +401,6 @@ } @Test - @Feature({"Browser", "Snackbar", "Security"}) public void testHighPriorityUpdateWithUpdatedText() { Activity activity = Robolectric.buildActivity(Activity.class).setup().get(); FrameLayout parent = new FrameLayout(activity); @@ -468,7 +454,6 @@ } @Test - @Feature({"Browser", "Snackbar", "Security"}) public void testHighPriorityUpdateWithUpdatedTemplateText() { SnackbarCollection collection = new SnackbarCollection(); @@ -501,7 +486,6 @@ } @Test - @Feature({"Browser", "Snackbar", "Security"}) public void testHighPriorityDeduplicationWithIdenticalText() { SnackbarCollection collection = new SnackbarCollection(); @@ -529,6 +513,110 @@ assertSame(hp1, collection.getCurrent()); } + @Test + public void testPersistentHighPriorityRestoredAfterActionPreemption() { + SnackbarCollection collection = new SnackbarCollection(); + SnackbarController persistentController = mock(SnackbarController.class); + SnackbarController actionController = mock(SnackbarController.class); + + Snackbar persistentHp = makePersistentHpSnackbar(persistentController); + collection.add(persistentHp); + assertEquals(persistentHp, collection.getCurrent()); + + Snackbar actionHp = + Snackbar.make( + "Press Esc to exit fullscreen", + actionController, + Snackbar.TYPE_ACTION, + Snackbar.UMA_EXCLUSIVE_ACCESS_BUBBLE) + .setHighPriority(true); + collection.add(actionHp); + assertEquals(actionHp, collection.getCurrent()); + verify(persistentController, org.mockito.Mockito.never()).onDismissNoAction(null); + + collection.removeCurrentDueToTimeout(); + verify(actionController, times(1)).onDismissNoAction(null); + assertEquals(persistentHp, collection.getCurrent()); ... (file diff truncated)
Loading diff…
Original Bug Report
The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.
References
On This Page