Low watch CVSS 4.3 chrome Spoofing 🔧 Commit mapped

Overview

Low
Severity
4.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUI misrepresentation in FullScreen
DescriptionUI misrepresentation in FullScreen
ComponentFullScreen
Sub-componentAndroid
Bug ClassSpoofing
Action tierwatch — logic/policy bug — security-relevant, no memory-unsafety
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker503787232
Fix commit3e4d359b7a4d (chromium/src) +553/-116
CWECWE-451
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:L/A:N
CISA KEVNot listed
CreditedFrancesco Topol (k4tedu)
Reported2026-04-18 (per vendor advisory)
Fix landed2026-06-11 (public git)
Disclosed2026-09-01
Patch gap82 days — fix public in source before the advisory

Background

Exclusive Access Bubble
the on-screen notice (“Press Esc to exit fullscreen”, etc.) that Chrome shows when a page enters fullscreen/pointer-lock so the user knows an untrusted page controls the whole screen.
Snackbar / `SnackbarManager` / `SnackbarCollection`
Android’s transient bottom-of-screen notification widget, its queue manager, and the data structure that holds queued snackbars; on Android the exclusive-access notice is rendered as a high-priority snackbar.
`setHighPriority` / `TYPE_ACTION`
a snackbar flag that shields security-critical notices from being discarded by other snackbars’ timeouts; TYPE_ACTION snackbars are the short-lived, user-action snackbars that share a queue.
Snooze / `OnUserInput`
the mechanism whereby user interaction resets (“snoozes”) the bubble’s hide timer instead of re-showing it, so the notice is not spammed on every keystroke.

Root Cause Analysis

On Android the exclusive-access notice’s lifetime was governed by a native timer whose expiry called ExclusiveAccessBubbleAndroid::Hide(), which unconditionally set was_shown_ = false and told the bridge to hide the snackbar even when the notice had been queued behind, or was not yet visible under, other UI — so the security notice could be dismissed before the user ever saw it. Compounding this, OnUserInput re-snoozed the bubble’s hide timer on every interaction with no bound, letting a page that keeps generating user-input-like activity indefinitely suppress the notice, and ordinary (non-high-priority) snackbars could crowd out or time out the notice in the shared SnackbarCollection queue.

The patch inverts the timer model: Hide() becomes a deliberate no-op that ignores the native timer dismissal, HideImmediately() (now also called from the destructor) performs the real teardown, and the notice’s timeout is moved Java-side (EXCLUSIVE_ACCESS_SNACKBAR_DURATION_MS = 3800) so the countdown only starts once the snackbar is actually visible. To defeat suppression-by-interaction, OnExclusiveAccessUserInput now counts snooze resets and, once snooze_reset_count_ reaches kMaxSnoozeResets (10), forces a re-show by setting force_params.force_update = true to bypass the was_shown_ latch. Finally SnackbarCollection gives high-priority security snackbars an isolated “VIP” slot (mActiveHighPriority) that always wins getCurrent(), dedupes rapid duplicates, and enforces a queue cap so normal snackbars cannot starve or evict the notice, and the disclosure/privacy-sandbox notices are marked setHighPriority(true).

Key insight
The fullscreen security notice’s visibility was driven by a native timer and an unbounded interaction-snooze that could fire (or be reset) while the notice was hidden or queued behind other UI, so a page could keep the user in fullscreen without a visible warning; the fix makes native Hide() a no-op, times the notice Java-side only while visible, caps snooze resets to force a periodic re-show, and isolates high-priority notices in the snackbar queue.

Attack Path

  1. Enter fullscreen A malicious page calls requestFullscreen (or acquires pointer lock), causing Chrome to queue the exclusive-access notice snackbar.
  2. Suppress the notice The page either spams snackbar-generating actions to crowd the shared queue, rapidly oscillates fullscreen requests, or drives repeated interaction so the pre-patch Hide()/native timer dismisses the notice or OnUserInput keeps re-snoozing it, so the “press Esc to exit” warning never becomes visible.
  3. Spoof trusted UI With no visible fullscreen notice, the page paints fake browser chrome/address bar or a spoofed OS/security prompt over the full screen, and the user cannot tell they are in an untrusted fullscreen surface (UI misrepresentation).
  4. Mitigation forces the notice After the patch the Java-side duration only counts while the notice is visible, the high-priority snackbar cannot be evicted by normal snackbars, and after kMaxSnoozeResets interactions force_update forces a re-show, so the warning re-appears despite suppression attempts.

Impact Assessment

The advisory rates this as UI misrepresentation (spoofing) in FullScreen on Android; an attacker gains the ability to keep the user in a page-controlled fullscreen surface without the security notice being reliably shown, enabling address-bar/OS-dialog spoofing in the browser UI. Preconditions are that the user grants/enters fullscreen (or pointer lock) and interacts with the page; the patch demonstrates only notice suppression/UI spoofing, and no memory-safety or code-execution consequence is shown. The advisory does not state in-the-wild exploitation, and any escalation beyond spoofing is (inferred).

Changed Functions

FunctionChangeNotes
if
chrome/android/java/src/org/chromium/chrome/browser/ui/ExclusiveAccessBubble.java
modified
if
chrome/browser/ui/android/exclusive_access/exclusive_access_context_android.cc
modified
SnackbarCollection
chrome/browser/ui/messages/android/java/src/org/chromium/chrome/browser/ui/messages/snackbar/SnackbarCollection.java
modified
if
chrome/browser/ui/messages/android/java/src/org/chromium/chrome/browser/ui/messages/snackbar/SnackbarCollection.java
modified

Files Changed

  • components/browser_ui/widget/android/java/src/org/chromium/components/browser_ui/widget/text/TemplatePreservingTextView.java
  • chrome/browser/ui/messages/android/java/src/org/chromium/chrome/browser/ui/messages/snackbar/SnackbarCollection.java
  • chrome/browser/ui/android/exclusive_access/exclusive_access_context_android.h
  • chrome/browser/ui/messages/android/java/src/org/chromium/chrome/browser/ui/messages/snackbar/SnackbarManager.java
  • chrome/browser/ui/android/exclusive_access/exclusive_access_bubble_android.cc
  • chrome/android/java/src/org/chromium/chrome/browser/browserservices/ui/view/DisclosureSnackbar.java
  • tools/metrics/histograms/metadata/ui/enums.xml
  • tools/metrics/histograms/metadata/ui/histograms.xml
  • chrome/browser/ui/android/exclusive_access/exclusive_access_context_android.cc
  • chrome/browser/ui/messages/android/java/src/org/chromium/chrome/browser/ui/messages/snackbar/SnackbarCollectionUnitTest.java
  • chrome/android/java/src/org/chromium/chrome/browser/browserservices/ui/view/DisclosureInfobar.java
  • chrome/browser/ui/android/exclusive_access/exclusive_access_bubble_android_unittest.cc
  • chrome/browser/privacy_sandbox/android/java/src/org/chromium/chrome/browser/privacy_sandbox/PrivacySandboxSnackbarController.java
  • chrome/browser/ui/exclusive_access/exclusive_access_bubble.h
  • chrome/browser/ui/messages/android/java/src/org/chromium/chrome/browser/ui/messages/snackbar/Snackbar.java
  • chrome/android/java/src/org/chromium/chrome/browser/ui/ExclusiveAccessBubble.java
  • chrome/browser/ui/messages/android/java/src/org/chromium/chrome/browser/ui/messages/snackbar/SnackbarView.java

Audit Directions

  • Security notice lifetime tied to timers that run while hidden
    Review any warning/notice whose auto-dismiss timer starts before the notice is confirmed visible, or that can be dismissed while occluded/queued.
  • Unbounded snooze/reset on user input
    Look for handlers that reset a hide timer on every interaction with no cap, allowing a hostile page to suppress a mandatory notice indefinitely; ensure a forced re-show bound exists.
  • Shared notification queues mixing security and ordinary items
    Audit queue managers where high-priority/security items share ordering, timeout, or capacity with normal items and can be evicted, timed out, or crowded out.
From 3e4d359b7a4de442ecd9a5ecb2e8af37639d7568 Mon Sep 17 00:00:00 2001
From: Vikram Pasupathy <[email protected]>
Date: Thu, 11 Jun 2026 14:22:00 -0700
Subject: [PATCH] Android: Harden Snackbar system against security notice suppression

Implement structural improvements to Android's Snackbar and Exclusive
Access systems to prevent security notice bypasses and UI spoofing.

Design: http://shortn/_FiA9a66mBh
Bug: 517091927, 503787232, 514072194
Change-Id: Ie73529d96023e60fcaa9fb9ea28fafd73a8adae1
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7896882
Reviewed-by: Jinsuk Kim <[email protected]>
Reviewed-by: Foromo Daniel Soromou <[email protected]>
Commit-Queue: Vikram Pasupathy <[email protected]>
Reviewed-by: Avi Drissman <[email protected]>
Reviewed-by: Muyao Xu <[email protected]>
Reviewed-by: Sirisha Kavuluru <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1645607}
---
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/browserservices/ui/view/DisclosureInfobar.java b/chrome/android/java/src/org/chromium/chrome/browser/browserservices/ui/view/DisclosureInfobar.java
index d16bae42..d308fbca 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/browserservices/ui/view/DisclosureInfobar.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/browserservices/ui/view/DisclosureInfobar.java
@@ -102,6 +102,7 @@
         String action = mResources.getString(R.string.ok);
         return Snackbar.make(title, mSnackbarController, type, code)
                 .setAction(action, null)
+                .setHighPriority(true)
                 .setDefaultLines(false);
     }
 
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/browserservices/ui/view/DisclosureSnackbar.java b/chrome/android/java/src/org/chromium/chrome/browser/browserservices/ui/view/DisclosureSnackbar.java
index 4e7f159..37cf26c 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/browserservices/ui/view/DisclosureSnackbar.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/browserservices/ui/view/DisclosureSnackbar.java
@@ -61,6 +61,7 @@
 
         return Snackbar.make(title, controller, type, code)
                 .setAction(action, null)
+                .setHighPriority(true)
                 .setDuration(DURATION_MS)
                 .setDefaultLines(false);
     }
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 bc2646bf..5509167 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
@@ -18,6 +18,8 @@
  */
 @NullMarked
 public class ExclusiveAccessBubble {
+    private static final int EXCLUSIVE_ACCESS_SNACKBAR_DURATION_MS = 3800;
+
     private final ExclusiveAccessContext mParentContext;
     private @Nullable Snackbar mSnackbar;
     private final SnackbarManager.SnackbarController mSnackbarController =
@@ -52,7 +54,10 @@
                                     Snackbar.UMA_EXCLUSIVE_ACCESS_BUBBLE)
                             // The exclusive access notice is security-critical and should not
                             // be discarded by the timeout of other action snackbars in the queue.
-                            .setHighPriority(true);
+                            .setHighPriority(true)
+                            // Use a Java-side timeout so that the timer only starts when
+                            // the notice is actually visible to the user.
+                            .setDuration(EXCLUSIVE_ACCESS_SNACKBAR_DURATION_MS);
             snackbarManager.showSnackbar(mSnackbar);
         }
     }
@@ -60,7 +65,23 @@
     @CalledByNative
     public void update(String text) {
         if (mText != null && mText.equals(text) && mSnackbar != null) return;
+        SnackbarManager snackbarManager = mParentContext.getSnackbarManager();
+        if (snackbarManager == null) return;
+
         mText = text;
+        if (mSnackbar != null) {
+            mSnackbar =
+                    Snackbar.make(
+                                    text,
+                                    mSnackbarController,
+                                    Snackbar.TYPE_ACTION,
+                                    Snackbar.UMA_EXCLUSIVE_ACCESS_BUBBLE)
+                            .setHighPriority(true)
+                            .setDuration(EXCLUSIVE_ACCESS_SNACKBAR_DURATION_MS);
+            // This will trigger SnackbarManager.updateView() and update the existing view.
+            snackbarManager.showSnackbar(mSnackbar);
+            return;
+        }
         hide();
         show();
     }
diff --git a/chrome/browser/privacy_sandbox/android/java/src/org/chromium/chrome/browser/privacy_sandbox/PrivacySandboxSnackbarController.java b/chrome/browser/privacy_sandbox/android/java/src/org/chromium/chrome/browser/privacy_sandbox/PrivacySandboxSnackbarController.java
index 05d29df..ca887f1 100644
--- a/chrome/browser/privacy_sandbox/android/java/src/org/chromium/chrome/browser/privacy_sandbox/PrivacySandboxSnackbarController.java
+++ b/chrome/browser/privacy_sandbox/android/java/src/org/chromium/chrome/browser/privacy_sandbox/PrivacySandboxSnackbarController.java
@@ -41,6 +41,7 @@
                                 Snackbar.TYPE_PERSISTENT,
                                 Snackbar.UMA_PRIVACY_SANDBOX_PAGE_OPEN)
                         .setAction(mContext.getString(R.string.more), null)
+                        .setHighPriority(true)
                         .setDefaultLines(false));
     }
 
diff --git a/chrome/browser/ui/android/exclusive_access/exclusive_access_bubble_android.cc b/chrome/browser/ui/android/exclusive_access/exclusive_access_bubble_android.cc
index 3ffe446..dccce5e6 100644
--- a/chrome/browser/ui/android/exclusive_access/exclusive_access_bubble_android.cc
+++ b/chrome/browser/ui/android/exclusive_access/exclusive_access_bubble_android.cc
@@ -96,12 +96,14 @@
 }
 
 ExclusiveAccessBubbleAndroid::~ExclusiveAccessBubbleAndroid() {
-  Hide();
+  HideImmediately();
 }
 
 void ExclusiveAccessBubbleAndroid::Hide() {
-  was_shown_ = false;
-  bridge_->Hide();
+  // On Android, the exclusive access notice is managed by a Java-side timer
+  // that only starts once the snackbar is visible. We ignore the native
+  // timer dismissal here to prevent the notice from being hidden while
+  // it is covered by other UI elements.
 }
 
 void ExclusiveAccessBubbleAndroid::Show() {
@@ -110,7 +112,8 @@
 }
 
 void ExclusiveAccessBubbleAndroid::HideImmediately() {
-  Hide();
+  was_shown_ = false;
+  bridge_->Hide();
 }
 
 void ExclusiveAccessBubbleAndroid::Update(
diff --git a/chrome/browser/ui/android/exclusive_access/exclusive_access_context_android.cc b/chrome/browser/ui/android/exclusive_access/exclusive_access_context_android.cc
index 61b736fa..312462a 100644
--- a/chrome/browser/ui/android/exclusive_access/exclusive_access_context_android.cc
+++ b/chrome/browser/ui/android/exclusive_access/exclusive_access_context_android.cc
@@ -22,8 +22,15 @@
       j_activity_tab_provider));
 }
 
+ExclusiveAccessContextAndroid::ExclusiveAccessContextAndroid() = default;
+
 ExclusiveAccessContextAndroid::~ExclusiveAccessContextAndroid() = default;
 
+void ExclusiveAccessContextAndroid::SetBubbleForTesting(  // IN-TEST
+    std::unique_ptr<ExclusiveAccessBubbleAndroid> bubble) {
+  exclusive_access_bubble_ = std::move(bubble);
+}
+
 void ExclusiveAccessContextAndroid::Destroy(JNIEnv* env) {
   Java_ExclusiveAccessContext_destroy(env, java_context_);
 }
@@ -119,6 +126,7 @@
 void ExclusiveAccessContextAndroid::DestroyAnyExclusiveAccessBubble() {
   exclusive_access_bubble_.reset();
   exclusive_access_bubble_destruction_task_id_.reset();
+  snooze_reset_count_ = 0;
 }
 
 bool ExclusiveAccessContextAndroid::IsExclusiveAccessBubbleDisplayed() const {
@@ -130,7 +138,20 @@
   // re-snooze the bubble's hide timer or re-show it if the snooze period has
   // elapsed.
   if (exclusive_access_bubble_) {
-    exclusive_access_bubble_->OnUserInput();
+    // Prevent a malicious site from indefinitely suppressing the security
+    // notice by repeatedly resetting the snooze timer via interaction.
+    // If the user has interacted many times, we force a re-show regardless.
+    if (++snooze_reset_count_ >= kMaxSnoozeResets) {
+      snooze_reset_count_ = 0;
+      ExclusiveAccessBubbleParams force_params =
+          exclusive_access_bubble_->params();
+      // Set force_update to true to bypass the was_shown_ check and force
+      // the notice to re-show.
+      force_params.force_update = true;
+      exclusive_access_bubble_->Update(force_params, base::NullCallback());
+    } else {
+      exclusive_access_bubble_->OnUserInput();
+    }
   }
 }
 
diff --git a/chrome/browser/ui/android/exclusive_access/exclusive_access_context_android.h b/chrome/browser/ui/android/exclusive_access/exclusive_access_context_android.h
index 52e5dc1..c9dc89d 100644
--- a/chrome/browser/ui/android/exclusive_access/exclusive_access_context_android.h
+++ b/chrome/browser/ui/android/exclusive_access/exclusive_access_context_android.h
@@ -22,8 +22,14 @@
       const jni_zero::JavaRef<jobject>& j_context,
       const jni_zero::JavaRef<jobject>& j_fullscreen_manager,
       const jni_zero::JavaRef<jobject>& j_activity_tab_provider);
+  // Constructor for testing, bypasses JNI creation.
+  ExclusiveAccessContextAndroid();
   ~ExclusiveAccessContextAndroid() override;
 
+  // Injects an exclusive access bubble instance for testing.
+  void SetBubbleForTesting(
+      std::unique_ptr<ExclusiveAccessBubbleAndroid> bubble);
+
   void Destroy(JNIEnv* env);
 
   bool IsFullscreen() const override;
@@ -70,11 +76,14 @@
 
   void ForceActiveTab(JNIEnv* env, const jni_zero::JavaRef<jobject>& j_tab);
 
+ private:
+  int snooze_reset_count_ = 0;
+  static constexpr int kMaxSnoozeResets = 10;
+
   base::WeakPtr<ExclusiveAccessContextAndroid> GetAsWeakPtr() {
     return weak_ptr_factory_.GetWeakPtr();
   }
 
- private:
   void DestroyAnyExclusiveAccessBubble();
 
   base::android::ScopedJavaGlobalRef<jobject> java_context_;
diff --git a/chrome/browser/ui/exclusive_access/exclusive_access_bubble.h b/chrome/browser/ui/exclusive_access/exclusive_access_bubble.h
index 376bae87..6325a826 100644
--- a/chrome/browser/ui/exclusive_access/exclusive_access_bubble.h
+++ b/chrome/browser/ui/exclusive_access/exclusive_access_bubble.h
@@ -24,6 +24,9 @@
   // Called on user input to update timers and/or re-show the bubble.
   void OnUserInput();
 
+  // Returns the parameters used to configure the bubble.
+  const ExclusiveAccessBubbleParams& params() const { return params_; }
+
   // Time the bubble is shown before hiding automatically.
   static constexpr base::TimeDelta kShowTime = base::Milliseconds(3800);
   // Time without user input that must elapse before the bubble is re-shown.
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 9099770..72c7d65 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
@@ -31,7 +31,7 @@
 public class Snackbar {
     /**
      * Snackbars that are created as an immediate response to user's action. These snackbars are
-     * managed in a stack and will be swiped away altogether after timeout.
+     * managed in a stack and will be dismissed one by one.
      */
     public static final int TYPE_ACTION = 0;
 
@@ -205,7 +205,7 @@
     /**
      * Sets whether the snackbar is high priority. High priority snackbars are shielded from being
      * discarded by the timeout of other action-type snackbars in the queue (e.g. for security-
-     * critical notices).
+     * critical notices), and they stay at the front of the queue.
      */
     public Snackbar setHighPriority(boolean highPriority) {
         mIsHighPriority = highPriority;
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 bac0e5bb..bb05f57e 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
@@ -15,87 +15,148 @@
 import org.chromium.chrome.browser.ui.messages.snackbar.SnackbarManager.SnackbarController;
 
 import java.util.ArrayDeque;
+import java.util.ArrayList;
 import java.util.Deque;
 import java.util.Iterator;
+import java.util.List;
 
 /** A data structure that holds all the {@link Snackbar}s managed by {@link SnackbarManager}. */
 @NullMarked
 class SnackbarCollection {
+    // Standard FIFO queue for normal notifications (e.g., "Copied to clipboard").
     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;
 
-    /**
-     * Adds a new snackbar to the collection. If the new snackbar is of
-     * {@link Snackbar#TYPE_ACTION} and current snackbar is of
-     * {@link Snackbar#TYPE_NOTIFICATION}, the current snackbar will be removed from the
-     * collection immediately. Snackbars of {@link Snackbar#TYPE_PERSISTENT} will appear after all
-     * action and notification type snackbars are dismissed and will be hidden if an action or
-     * notifications are added after it.
-     */
+    private static final int MAX_SNACKBARS = 10;
+
     void add(Snackbar snackbar) {
-        if (snackbar.isTypeAction()) {
+        RecordHistogram.recordExactLinearHistogram(
+                "Snackbar.QueueDepthAtInsertion",
+                mSnackbars.size()
+                        + mPersistentSnackbars.size()
+                        + (mActiveHighPriority != null ? 1 : 0),
+                MAX_SNACKBARS + 1);
+
+        if (snackbar.isHighPriority()) {
+            // High Priority (HP) Security Warnings bypass the regular queues entirely.
+            if (mActiveHighPriority != 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()
+                        && objectsAreEqual(
+                                mActiveHighPriority.getActionData(), snackbar.getActionData())) {
+                    return; // Deduplication: already have this HP snackbar.
+                }
+                // LIFO Immediate Interruption: The new HP warning instantly overwrites the old one.
+                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());
+                }
+            }
+            // Assign to the VIP slot. getCurrent() will now prioritize this above all else.
+            mActiveHighPriority = snackbar;
+            return;
+        }
+
+        if (snackbar.isTypePersistent()) {
+            assert !TextUtils.isEmpty(snackbar.getActionText())
+                    : "Persistent snackbars require action text.";
+            mPersistentSnackbars.addFirst(snackbar);
+        } else if (snackbar.isTypeAction()) {
             if (getCurrent() != null && !getCurrent().isTypeAction()) {
                 removeCurrent(DismissalReason.REPLACED_BY_ACTION_SNACKBAR);
             }
             mSnackbars.addFirst(snackbar);
-        } else if (snackbar.isTypePersistent()) {
-            // Although persistent snackbars set their action text by default, it is possible that
-            // the developer overrides it. This is a safeguard to ensure all persistent snackbars
-            // have a method of dismissal.
-            assert !TextUtils.isEmpty(snackbar.getActionText())
-                    : "Persistent snackbars require action text.";
-            mPersistentSnackbars.addFirst(snackbar);
         } else {
             mSnackbars.addLast(snackbar);
         }
+
+        // Enforce a hard capacity limit to prevent queue exhaustion.
+        // Drop the oldest unseen standard notifications from the front of the queue.
+        while (mSnackbars.size() > MAX_SNACKBARS) {
+            removeOldestUnseenSnackbar(mSnackbars);
+        }
+        while (mPersistentSnackbars.size() > MAX_SNACKBARS) {
+            removeOldestUnseenSnackbar(mPersistentSnackbars);
+        }
     }
 
-    /**
-     * Removes the current snackbar from the collection after the user has clicked on the action
-     * button.
-     */
-    Snackbar removeCurrentDueToAction() {
+    private void removeOldestUnseenSnackbar(Deque<Snackbar> list) {
+        Iterator<Snackbar> iter = list.iterator();
+        if (iter.hasNext() && list.peekFirst() == getCurrent()) {
+            iter.next(); // Skip the currently showing one
+        }
+        if (!iter.hasNext()) return;
+        Snackbar toRemove = iter.next();
+        iter.remove();
+        RecordHistogram.recordEnumeratedHistogram(
+                "Snackbar.DismissalReason", DismissalReason.OTHERS, DismissalReason.NUM_ENTRIES);
+        SnackbarController controller = toRemove.getController();
+        if (controller != null) {
+            controller.onDismissNoAction(toRemove.getActionData());
+        }
+    }
+
+    boolean contains(Snackbar snackbar) {
+        return snackbar == mActiveHighPriority
+                || mSnackbars.contains(snackbar)
+                || mPersistentSnackbars.contains(snackbar);
+    }
+
+    @Nullable Snackbar getCurrent() {
+        if (mActiveHighPriority != null) return mActiveHighPriority;
+        Snackbar actionCurrent = mSnackbars.peekFirst();
+        Snackbar persistentCurrent = mPersistentSnackbars.peekFirst();
+        return actionCurrent != null ? actionCurrent : persistentCurrent;
+    }
+
+    @Nullable Snackbar removeCurrentDueToAction() {
         return removeCurrent(DismissalReason.ACTION_BUTTON);
     }
 
-    /** Removes the current snackbar from the collection after the user has swiped it away. */
     @Nullable Snackbar removeCurrentDueToSwipe() {
         return removeCurrent(DismissalReason.SWIPE);
     }
... (file diff truncated)
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/chrome/browser/ui/android/exclusive_access/exclusive_access_bubble_android_unittest.cc b/chrome/browser/ui/android/exclusive_access/exclusive_access_bubble_android_unittest.cc
index c29f21816c..976f848 100644
--- a/chrome/browser/ui/android/exclusive_access/exclusive_access_bubble_android_unittest.cc
+++ b/chrome/browser/ui/android/exclusive_access/exclusive_access_bubble_android_unittest.cc
@@ -4,8 +4,13 @@
 
 #include "chrome/browser/ui/android/exclusive_access/exclusive_access_bubble_android.h"
 
+#include <memory>
+#include <string>
+#include <utility>
+
 #include "base/android/jni_string.h"
 #include "base/functional/callback_helpers.h"
+#include "chrome/browser/ui/android/exclusive_access/exclusive_access_context_android.h"
 #include "chrome/test/base/chrome_render_view_host_test_harness.h"
 #include "testing/gmock/include/gmock/gmock.h"
 #include "testing/gtest/include/gtest/gtest.h"
@@ -106,4 +111,45 @@
   EXPECT_CALL(*mock_bridge_ptr, Hide()).Times(1);
 }
 
+TEST_F(ExclusiveAccessBubbleAndroidTest, SnoozeResetForciblyReshowsNotice) {
+  ExclusiveAccessBubbleParams params;
+  params.type = EXCLUSIVE_ACCESS_BUBBLE_TYPE_FULLSCREEN_EXIT_INSTRUCTION;
+
+  auto mock_bridge = std::make_unique<MockBridge>();
+  auto* mock_bridge_ptr = mock_bridge.get();
+
+  // Initial show on creation.
+  EXPECT_CALL(*mock_bridge_ptr, IsVisible()).WillOnce(Return(false));
+  EXPECT_CALL(*mock_bridge_ptr, IsKeyboardConnected()).WillOnce(Return(false));
+  EXPECT_CALL(*mock_bridge_ptr, Update(_)).Times(1);
+  EXPECT_CALL(*mock_bridge_ptr, Show()).Times(1);
+
+  auto bubble = std::make_unique<ExclusiveAccessBubbleAndroid>(
+      params, base::DoNothing(), std::move(mock_bridge));
+
+  testing::Mock::VerifyAndClearExpectations(mock_bridge_ptr);
+
+  ExclusiveAccessContextAndroid context;
+  context.SetBubbleForTesting(std::move(bubble));
+
+  // Verify that the first 9 user inputs don't trigger any show or update on the
+  // bridge.
+  for (int i = 1; i <= 9; ++i) {
+    context.OnExclusiveAccessUserInput();
+  }
+
+  // The 10th user input exceeds the snooze interaction threshold and must
+  // forcibly re-show the security notice regardless of whether it was
+  // previously shown in this session (i.e. force_update is set to true to
+  // override the was_shown_ latch).
+  EXPECT_CALL(*mock_bridge_ptr, IsVisible()).WillOnce(Return(false));
+  EXPECT_CALL(*mock_bridge_ptr, IsKeyboardConnected()).WillOnce(Return(false));
+  EXPECT_CALL(*mock_bridge_ptr, Update(_)).Times(1);
+  EXPECT_CALL(*mock_bridge_ptr, Show()).Times(1);
+
+  context.OnExclusiveAccessUserInput();
+
+  testing::Mock::VerifyAndClearExpectations(mock_bridge_ptr);
+}
+
 }  // namespace
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.