Chrome · Messages › Chrome
CVE-2026-113156
UAF in Messages (Chrome)
Overview
Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchrome/android/java/src/org/chromium/chrome/browser/ui/ExclusiveAccessBubble.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/Snackbar.javachrome/browser/ui/messages/android/BUILD.gnchrome/android/junit/src/org/chromium/chrome/browser/ui/ExclusiveAccessBubbleTest.javachrome/browser/ui/messages/android/java/src/org/chromium/chrome/browser/ui/messages/snackbar/SnackbarCollectionUnitTest.javachrome/android/java/src/org/chromium/chrome/browser/ui/ExclusiveAccessBubble.java
Patch
From 44d4c33701768dd21cad4652307831a5831bfc2a Mon Sep 17 00:00:00 2001
From: Vikram Pasupathy <vpasupathy@chromium.org>
Date: Tue, 22 Sep 2026 15:55:20 -0700
Subject: [PATCH] chrome: Update active high-priority snackbar when text changes during deduplication
When a high-priority snackbar is submitted by the same controller with
updated text (such as updating fullscreen exit instructions from 'Press
Esc' to 'Press and hold Esc'), the deduplication check previously
treated it as a duplicate and dropped the update entirely.
This CL checks whether the text or template text changed before
deduplicating. If it changed, mActiveHighPriority is updated to the new
snackbar instance without dismissing, ensuring the updated notice is
displayed and its timeout reset.
Repro of issue: http://shortn/_IOFOwfldT5
Patched recording: http://shortn/_4jBjRiT8XK
In the repro, the second snackbar isn't shown when keyboard lock is
acquired, leaving the exit instruction stuck on 'Press |Esc|' instead of
updating to 'Press and hold │Esc|'.
In the recording of the fix, the showSnackbar updates properly.
Bug: 553253820
Change-Id: I5614ceb94f287a5356705b0d82b398648bebf3fe
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8393898
Reviewed-by: Charles Hager <clhager@google.com>
Commit-Queue: Vikram Pasupathy <vpasupathy@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1703025}
---
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/ui/ExclusiveAccessBubble.java b/chrome/android/java/src/org/chromium/chrome/browser/ui/ExclusiveAccessBubble.java
index 35bf088..984e2ac2 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/ui/ExclusiveAccessBubble.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/ui/ExclusiveAccessBubble.java
@@ -69,14 +69,7 @@
SnackbarManager snackbarManager = mParentContext.getSnackbarManager();
if (snackbarManager != null && mSnackbar != null) {
- mSnackbar =
- Snackbar.make(
- text,
- mSnackbarController,
- Snackbar.TYPE_ACTION,
- Snackbar.UMA_EXCLUSIVE_ACCESS_BUBBLE)
- .setHighPriority(true)
- .setDuration(EXCLUSIVE_ACCESS_SNACKBAR_DURATION_MS);
+ mSnackbar.setText(text);
// This will trigger SnackbarManager.updateView() and update the existing view.
snackbarManager.showSnackbar(mSnackbar);
return;
diff --git a/chrome/android/junit/src/org/chromium/chrome/browser/ui/ExclusiveAccessBubbleTest.java b/chrome/android/junit/src/org/chromium/chrome/browser/ui/ExclusiveAccessBubbleTest.java
index 49b5afe..4e320678 100644
--- a/chrome/android/junit/src/org/chromium/chrome/browser/ui/ExclusiveAccessBubbleTest.java
+++ b/chrome/android/junit/src/org/chromium/chrome/browser/ui/ExclusiveAccessBubbleTest.java
@@ -9,12 +9,14 @@
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import org.chromium.base.test.BaseRobolectricTestRunner;
+import org.chromium.chrome.browser.ui.messages.snackbar.Snackbar;
import org.chromium.chrome.browser.ui.messages.snackbar.SnackbarManager;
/** Tests for {@link ExclusiveAccessBubble}. */
@@ -68,4 +70,23 @@
bubble.update("Test text");
Mockito.verify(mSnackbarManager, Mockito.times(2)).showSnackbar(Mockito.any());
}
+
+ @Test
+ public void testUpdateWithNewTextReusesSnackbarInstance() {
+ ExclusiveAccessBubble bubble = ExclusiveAccessBubble.create(mExclusiveAccessContext);
+
+ bubble.update("Press Esc");
+ ArgumentCaptor<Snackbar> captor = ArgumentCaptor.forClass(Snackbar.class);
+ Mockito.verify(mSnackbarManager, Mockito.times(1)).showSnackbar(captor.capture());
+ Snackbar firstSnackbar = captor.getValue();
+ Assert.assertEquals("Press Esc", firstSnackbar.getTextForTesting());
+
+ bubble.update("Press and hold Esc");
+ Mockito.verify(mSnackbarManager, Mockito.times(2)).showSnackbar(captor.capture());
+ Snackbar secondSnackbar = captor.getValue();
+
+ Assert.assertSame(firstSnackbar, secondSnackbar);
+ Assert.assertEquals("Press and hold Esc", secondSnackbar.getTextForTesting());
+ Mockito.verify(mSnackbarManager, Mockito.never()).dismissSnackbars(Mockito.any());
+ }
}
diff --git a/chrome/browser/ui/messages/android/java/src/org/chromium/chrome/browser/ui/messages/snackbar/Snackbar.java b/chrome/browser/ui/messages/android/java/src/org/chromium/chrome/browser/ui/messages/snackbar/Snackbar.java
index 0143563..6f4b9455c 100644
--- a/chrome/browser/ui/messages/android/java/src/org/chromium/chrome/browser/ui/messages/snackbar/Snackbar.java
+++ b/chrome/browser/ui/messages/android/java/src/org/chromium/chrome/browser/ui/messages/snackbar/Snackbar.java
@@ -18,6 +18,7 @@
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
/**
* A snackbar shows a message at the bottom of the screen and optionally contains an action button.
@@ -156,7 +157,7 @@
// LINT.ThenChange(//tools/metrics/histograms/metadata/ui/enums.xml:SnackbarIdentifier)
private final @Nullable SnackbarController mController;
- private final @Nullable CharSequence mText;
+ private @Nullable CharSequence mText;
private @Nullable String mTemplateText;
private @Nullable String mActionText;
private @Nullable Object mActionData;
@@ -223,6 +224,12 @@
return this;
}
+ /** Sets the text to show on the snackbar. */
+ public Snackbar setText(@Nullable CharSequence text) {
+ mText = text;
+ return this;
+ }
+
/**
* Sets the template text to show on the snackbar, e.g. "Closed %s". See {@link
* TemplatePreservingTextView} for details on how the template text is used.
@@ -390,4 +397,43 @@
public @Nullable CharSequence getTextForTesting() {
return mText;
}
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) return true;
+ if (!(obj instanceof Snackbar other)) return false;
+ return mController == other.mController
+ && Objects.equals(mText, other.mText)
+ && Objects.equals(mTemplateText, other.mTemplateText)
+ && Objects.equals(mActionText, other.mActionText)
+ && Objects.equals(mActionData, other.mActionData)
+ && mBackgroundColor == other.mBackgroundColor
+ && mTextAppearanceResId == other.mTextAppearanceResId
+ && mDefaultLines == other.mDefaultLines
+ && mIsHighPriority == other.mIsHighPriority
+ && mDurationMs == other.mDurationMs
+ && Objects.equals(mProfileImage, other.mProfileImage)
+ && mType == other.mType
+ && mIdentifier == other.mIdentifier
+ && mTheme == other.mTheme;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(
+ mController,
+ mText,
+ mTemplateText,
+ mActionText,
+ mActionData,
+ mBackgroundColor,
+ mTextAppearanceResId,
+ mDefaultLines,
+ mIsHighPriority,
+ mDurationMs,
+ mProfileImage,
+ mType,
+ mIdentifier,
+ mTheme);
+ }
}
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 bb05f57e..e7dc81e1 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
@@ -19,6 +19,7 @@
import java.util.Deque;
import java.util.Iterator;
import java.util.List;
+import java.util.Objects;
/** A data structure that holds all the {@link Snackbar}s managed by {@link SnackbarManager}. */
@NullMarked
@@ -47,8 +48,14 @@
// 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()
- && objectsAreEqual(
+ && Objects.equals(
mActiveHighPriority.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;
+ }
return; // Deduplication: already have this HP snackbar.
}
// LIFO Immediate Interruption: The new HP warning instantly overwrites the old one.
@@ -217,7 +224,7 @@
boolean removed = false;
if (mActiveHighPriority != null
&& mActiveHighPriority.getController() == controller
- && objectsAreEqual(mActiveHighPriority.getActionData(), data)) {
+ && Objects.equals(mActiveHighPriority.getActionData(), data)) {
Snackbar removedHp = mActiveHighPriority;
mActiveHighPriority = null;
RecordHistogram.recordEnumeratedHistogram(
@@ -250,7 +257,7 @@
while (iter.hasNext()) {
Snackbar snackbar = iter.next();
if (snackbar.getController() == controller
- && objectsAreEqual(snackbar.getActionData(), data)) {
+ && Objects.equals(snackbar.getActionData(), data)) {
iter.remove();
removedSnackbars.add(snackbar);
}
@@ -262,10 +269,4 @@
}
return !removedSnackbars.isEmpty();
}
-
- private static boolean objectsAreEqual(@Nullable Object a, @Nullable Object b) {
- if (a == null && b == null) return true;
- if (a == null || b == null) return false;
- return a.equals(b);
- }
}
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 7d578ca..9e4feb0 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
@@ -6,27 +6,38 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
+import android.app.Activity;
+import android.graphics.Color;
+import android.widget.FrameLayout;
+
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
-import org.junit.runners.BlockJUnit4ClassRunner;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
+import org.robolectric.Robolectric;
+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;
+import java.util.concurrent.TimeUnit;
+
/** Tests for {@link SnackbarCollection}. */
-@RunWith(BlockJUnit4ClassRunner.class)
+@RunWith(BaseRobolectricTestRunner.class)
public class SnackbarCollectionUnitTest {
private static final String ACTION_TITLE = "stack";
private static final String NOTIFICATION_TITLE = "queue";
@@ -402,6 +413,122 @@
assertTrue(collection.isEmpty());
}
+ @Test
+ @Feature({"Browser", "Snackbar", "Security"})
+ public void testHighPriorityUpdateWithUpdatedText() {
+ Activity activity = Robolectric.buildActivity(Activity.class).setup().get();
+ FrameLayout parent = new FrameLayout(activity);
+ activity.setContentView(parent);
+ SnackbarManager manager = new SnackbarManager(activity, parent, null, null, null);
+
+ Snackbar hp1 =
+ Snackbar.make(
+ "Press Esc",
+ mMockController,
+ Snackbar.TYPE_ACTION,
+ Snackbar.UMA_EXCLUSIVE_ACCESS_BUBBLE)
+ .setBackgroundColor(Color.BLACK)
+ .setHighPriority(true);
+ manager.showSnackbar(hp1);
+ assertEquals(hp1, manager.getCurrentSnackbarForTesting());
+ assertEquals("Press Esc", manager.getCurrentSnackbarForTesting().getText());
+
+ // Advance 7000ms into the 10000ms TYPE_ACTION timeout (3000ms remaining).
+ ShadowLooper.idleMainLooper(7000, TimeUnit.MILLISECONDS);
+ assertTrue(manager.isShowing());
+
+ Snackbar hp2 =
+ Snackbar.make(
+ "Press and hold Esc",
+ mMockController,
+ Snackbar.TYPE_ACTION,
+ Snackbar.UMA_EXCLUSIVE_ACCESS_BUBBLE)
+ .setBackgroundColor(Color.BLACK)
+ .setHighPriority(true);
+ manager.showSnackbar(hp2);
+
+ // Verify hp1 was updated to hp2 without dismissing the controller.
+ verify(mMockController, never()).onDismissNoAction(any());
+ assertEquals(hp2, manager.getCurrentSnackbarForTesting());
+ assertEquals("Press and hold Esc", manager.getCurrentSnackbarForTesting().getText());
+
+ // Advance another 7000ms (14000ms total elapsed > initial 10000ms duration).
+ // Because the duration timeout was reset on update, the snackbar must still be showing.
+ ShadowLooper.idleMainLooper(7000, TimeUnit.MILLISECONDS);
+ verify(mMockController, never()).onDismissNoAction(any());
+ assertTrue(manager.isShowing());
+ assertEquals(hp2, manager.getCurrentSnackbarForTesting());
+
+ // Advance past the remaining 3000ms of the reset 10000ms duration; now it should time out.
+ ShadowLooper.idleMainLooper(3500, TimeUnit.MILLISECONDS);
+ verify(mMockController, times(1)).onDismissNoAction(null);
+ assertFalse(manager.isShowing());
+ manager.destroy();
+ ShadowLooper.idleMainLooper();
+ }
+
+ @Test
+ @Feature({"Browser", "Snackbar", "Security"})
+ public void testHighPriorityUpdateWithUpdatedTemplateText() {
+ SnackbarCollection collection = new SnackbarCollection();
+
+ Snackbar hp1 =
+ Snackbar.make(
+ "Title",
+ mMockController,
+ Snackbar.TYPE_ACTION,
+ Snackbar.UMA_EXCLUSIVE_ACCESS_BUBBLE)
+ .setTemplateText("Template %s")
+ .setHighPriority(true)
+ .setDuration(3800);
+ collection.add(hp1);
+ assertEquals(hp1, collection.getCurrent());
+
+ Snackbar hp2 =
+ Snackbar.make(
+ "Title",
+ mMockController,
+ Snackbar.TYPE_ACTION,
+ Snackbar.UMA_EXCLUSIVE_ACCESS_BUBBLE)
+ .setTemplateText("Updated Template %s")
+ .setHighPriority(true)
+ .setDuration(5000);
+ collection.add(hp2);
+
+ verify(mMockController, never()).onDismissNoAction(any());
+ assertEquals(hp2, collection.getCurrent());
+ assertEquals(5000, collection.getCurrent().getDuration());
+ }
+
+ @Test
+ @Feature({"Browser", "Snackbar", "Security"})
+ public void testHighPriorityDeduplicationWithIdenticalText() {
+ SnackbarCollection collection = new SnackbarCollection();
+
+ Snackbar hp1 =
+ Snackbar.make(
+ "Press Esc",
+ mMockController,
+ Snackbar.TYPE_ACTION,
+ Snackbar.UMA_EXCLUSIVE_ACCESS_BUBBLE)
+ .setHighPriority(true);
+ collection.add(hp1);
+ assertEquals(hp1, collection.getCurrent());
+
+ Snackbar hp2 =
+ Snackbar.make(
+ "Press Esc",
+ mMockController,
+ Snackbar.TYPE_ACTION,
+ Snackbar.UMA_EXCLUSIVE_ACCESS_BUBBLE)
+ .setHighPriority(true);
+ collection.add(hp2);
+
+ // Deduplicated without dismissal: the existing instance hp1 should be retained.
+ verify(mMockController, never()).onDismissNoAction(any());
+ assertSame(hp1, collection.getCurrent());
+ }
+
private Snackbar makeActionSnackbar(SnackbarController controller) {
return Snackbar.make(
ACTION_TITLE, controller, Snackbar.TYPE_ACTION, Snackbar.UMA_TEST_SNACKBAR);
diff --git a/chrome/browser/ui/messages/android/BUILD.gn b/chrome/browser/ui/messages/android/BUILD.gn
index 8076af8..6aea71f 100644
... (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