Medium chrome Logic Error 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUI misrepresentation in FullScreen
DescriptionUI misrepresentation in FullScreen
ComponentFullScreen
Bug ClassLogic Error
Tracker517091927
Fix commit3e4d359b7a4d (chromium/src) +553/-116
CISA KEVNot listed
CreditedGoogle
Disclosed2026-09-01

Background

`Snackbar`
Android’s transient bottom-of-screen notification widget that Chrome reuses to render security-critical notices such as the fullscreen exit instruction.
`SnackbarManager`
the shared, single-queue controller that schedules, times out, and dismisses every Snackbar, whether it is a low-priority action snackbar or a security notice.
Exclusive Access Bubble
the “you are in fullscreen” / exclusive-access disclosure that must remain visible so the user knows the page has taken over the screen or pointer.
`setHighPriority`
a Snackbar builder flag that keeps a notice from being displaced or discarded by the timeouts of other snackbars sharing the queue.

Root Cause Analysis

On Android the fullscreen/exclusive-access disclosure (and the Trusted Web Activity and Privacy Sandbox notices) was enqueued as an ordinary-priority Snackbar in the shared SnackbarManager queue, so a page could inject or trigger other action snackbars that displaced or suppressed the security notice before the user ever saw it. Compounding this, the dismissal timer ran on the native side: ExclusiveAccessBubbleAndroid::Hide() (driven by the native timeout) would tear down the notice even while it was still queued behind or covered by other UI, so the countdown elapsed against a notice that was never actually visible — the invariant that a security disclosure must be shown for its full duration while visible was violated.

The fix marks every such notice setHighPriority(true) so it cannot be discarded by other snackbars, and moves the timeout to a Java-side setDuration(EXCLUSIVE_ACCESS_SNACKBAR_DURATION_MS) that only starts counting once the snackbar is genuinely displayed. It also neutralizes the native timer path by making Hide() a no-op and routing real teardown only through HideImmediately() (invoked from the destructor), so the notice is no longer dismissed while obscured.

Key insight
The core mistake was letting a security-critical fullscreen disclosure share priority and a native, visibility-blind timeout with ordinary snackbars, so it could be suppressed or timed out before the user saw it; the fix elevates it to high priority and ties its lifetime to a Java-side timer that starts only when the notice is actually on screen.

Attack Path

  1. Enter exclusive access A malicious or misleading page requests fullscreen (or pointer lock), causing Chrome to enqueue the exclusive-access disclosure Snackbar.
  2. Flood the queue The page triggers or coincides with other, ordinary-priority snackbars that share the single SnackbarManager queue and displace the security notice.
  3. Race the native timer Because the dismissal countdown runs natively via Hide() regardless of visibility, the disclosure’s duration elapses while it is still covered or queued.
  4. Silent suppression The user never sees a legible “you are in fullscreen” notice, leaving them unaware the page controls the whole screen.
  5. Spoof the UI With no disclosure shown, the page can imitate browser or system chrome and mislead the user about their context.

Impact Assessment

An attacker gains UI misrepresentation: they can enter fullscreen or exclusive access on Android without the user reliably seeing the security disclosure, enabling spoofing of trusted UI. This affects the browser (UI) process on Android and requires the user to visit a page that requests exclusive access and manipulates snackbar timing; it is an integrity/UX weakness rather than memory corruption or code execution.

Changed Functions

FunctionChangeNotes
if
chrome/android/java/src/org/chromium/chrome/browser/ui/ExclusiveAccessBubble.java
modified
TEST_F
chrome/browser/ui/android/exclusive_access/exclusive_access_bubble_android_unittest.cc
modified
for
chrome/browser/ui/android/exclusive_access/exclusive_access_bubble_android_unittest.cc
modified

Files Changed

  • chrome/android/java/src/org/chromium/chrome/browser/browserservices/ui/view/DisclosureInfobar.java
  • chrome/android/java/src/org/chromium/chrome/browser/browserservices/ui/view/DisclosureSnackbar.java
  • chrome/android/java/src/org/chromium/chrome/browser/ui/ExclusiveAccessBubble.java
  • chrome/browser/privacy_sandbox/android/java/src/org/chromium/chrome/browser/privacy_sandbox/PrivacySandboxSnackbarController.java
  • chrome/browser/ui/android/exclusive_access/exclusive_access_bubble_android.cc
  • chrome/browser/ui/android/exclusive_access/exclusive_access_bubble_android_unittest.cc

Audit Directions

  • Security notices sharing a general UI queue
    Audit every security-critical disclosure rendered via SnackbarManager (or similar shared toast/queue managers) to confirm it sets setHighPriority(true) and cannot be displaced by attacker-triggerable low-priority items.
  • Visibility-blind timeouts
    Flag any disclosure whose dismissal timer runs before or independently of actual on-screen visibility (e.g. native-side Hide() timers), since the mandated display duration can elapse while the notice is covered or queued.
  • Latched one-shot notices
    Review was_shown_-style latches and re-show paths (as exercised by SnoozeResetForciblyReshowsNotice) to ensure notices are forcibly re-displayed after enough user interaction rather than permanently suppressed.
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_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);
+}
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.