CVE-2026-8561
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ExclusiveAccessBubbleTestchrome/browser/ui/exclusive_access/exclusive_access_bubble_unittest.cc |
modified | |
TEST_Fchrome/browser/ui/exclusive_access/exclusive_access_bubble_unittest.cc |
modified | |
FullscreenControllerInteractiveTestchrome/browser/ui/exclusive_access/fullscreen_controller_interactive_browsertest.cc |
modified |
Files Changed
chrome/browser/ui/exclusive_access/exclusive_access_bubble.ccchrome/browser/ui/exclusive_access/exclusive_access_bubble.hchrome/browser/ui/exclusive_access/exclusive_access_bubble_unittest.ccchrome/browser/ui/exclusive_access/exclusive_access_test.ccchrome/browser/ui/exclusive_access/fullscreen_controller_interactive_browsertest.ccchrome/browser/ui/views/exclusive_access_bubble_views.cc
Patch
From fdac21de070f3523b7315a268092d528a542a6d6 Mon Sep 17 00:00:00 2001
From: Frank Liberato <liberato@chromium.org>
Date: Tue, 31 Mar 2026 16:30:44 -0700
Subject: [PATCH] Don't start the exclusive access bubble timer until commit
Bug: 343352552
Change-Id: If0f8704a23c51e1431f9919ea88b989f74de4a4c
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7615417
Reviewed-by: Mike Wasserman <msw@chromium.org>
Commit-Queue: Frank Liberato <liberato@chromium.org>
Reviewed-by: Muyao Xu <muyaoxu@google.com>
Cr-Commit-Position: refs/heads/main@{#1608174}
---
diff --git a/chrome/browser/ui/exclusive_access/exclusive_access_bubble.cc b/chrome/browser/ui/exclusive_access/exclusive_access_bubble.cc
index 3df48a00..3bc3c10 100644
--- a/chrome/browser/ui/exclusive_access/exclusive_access_bubble.cc
+++ b/chrome/browser/ui/exclusive_access/exclusive_access_bubble.cc
@@ -21,19 +21,22 @@
// Re-show the bubble if no user input occurred during the snooze period.
if (base::TimeTicks::Now() > snooze_until_) {
ShowAndStartTimers();
- return;
}
- // Restart the snooze period; to only re-show after a period of inactivity.
- snooze_until_ = base::TimeTicks::Now() + kSnoozeTime;
+ Snooze();
}
void ExclusiveAccessBubble::ShowAndStartTimers() {
Show();
+ StartHideTimer();
+}
- // Restart the timer to hide the bubble after a few seconds.
+void ExclusiveAccessBubble::StartHideTimer() {
hide_timeout_.Reset();
+ Snooze();
+}
+void ExclusiveAccessBubble::Snooze() {
// Restart the snooze period; to only re-show after a period of inactivity.
snooze_until_ = base::TimeTicks::Now() + kSnoozeTime;
}
diff --git a/chrome/browser/ui/exclusive_access/exclusive_access_bubble.h b/chrome/browser/ui/exclusive_access/exclusive_access_bubble.h
index 0051d623..376bae87 100644
--- a/chrome/browser/ui/exclusive_access/exclusive_access_bubble.h
+++ b/chrome/browser/ui/exclusive_access/exclusive_access_bubble.h
@@ -37,7 +37,13 @@
virtual void Show() = 0;
// Shows the bubble and sets up timers to auto-hide and snooze.
- void ShowAndStartTimers();
+ virtual void ShowAndStartTimers();
+
+ // Starts the timer to hide the bubble after a short time.
+ void StartHideTimer();
+
+ // Reset the timeout for user input before we auto-show again.
+ void Snooze();
// Cached content and traits for this bubble.
ExclusiveAccessBubbleParams params_;
diff --git a/chrome/browser/ui/exclusive_access/exclusive_access_bubble_unittest.cc b/chrome/browser/ui/exclusive_access/exclusive_access_bubble_unittest.cc
index 5b87012b..a129897 100644
--- a/chrome/browser/ui/exclusive_access/exclusive_access_bubble_unittest.cc
+++ b/chrome/browser/ui/exclusive_access/exclusive_access_bubble_unittest.cc
@@ -13,12 +13,13 @@
explicit MockExclusiveAccessBubble(ExclusiveAccessBubbleParams params)
: ExclusiveAccessBubble(params) {}
~MockExclusiveAccessBubble() override = default;
- MOCK_METHOD(void, Hide, (), (override));
MOCK_METHOD(void, Show, (), (override));
+ MOCK_METHOD(void, Hide, (), (override));
using ExclusiveAccessBubble::hide_timeout_;
using ExclusiveAccessBubble::ShowAndStartTimers;
using ExclusiveAccessBubble::snooze_until_;
+ using ExclusiveAccessBubble::StartHideTimer;
};
class ExclusiveAccessBubbleTest : public testing::Test {
@@ -63,3 +64,20 @@
task_environment_.FastForwardBy(base::Minutes(16));
bubble_.OnUserInput();
}
+
+TEST_F(ExclusiveAccessBubbleTest, StartHideTimerRestartsTimer) {
+ EXPECT_CALL(bubble_, Show()).Times(1);
+ bubble_.ShowAndStartTimers();
+ EXPECT_TRUE(bubble_.hide_timeout_.IsRunning());
+
+ task_environment_.FastForwardBy(base::Seconds(1));
+ auto remaining =
+ bubble_.hide_timeout_.desired_run_time() - base::TimeTicks::Now();
+ EXPECT_LT(remaining, ExclusiveAccessBubble::kShowTime);
+
+ // Verify that the time increases.
+ bubble_.StartHideTimer();
+ auto new_remaining =
+ bubble_.hide_timeout_.desired_run_time() - base::TimeTicks::Now();
+ EXPECT_LT(remaining, new_remaining);
+}
diff --git a/chrome/browser/ui/exclusive_access/exclusive_access_test.cc b/chrome/browser/ui/exclusive_access/exclusive_access_test.cc
index 2756a83..91fd829 100644
--- a/chrome/browser/ui/exclusive_access/exclusive_access_test.cc
+++ b/chrome/browser/ui/exclusive_access/exclusive_access_test.cc
@@ -74,6 +74,8 @@
ExclusiveAccessTest::~ExclusiveAccessTest() = default;
void ExclusiveAccessTest::SetUpOnMainThread() {
+ ExclusiveAccessBubbleViews::set_skip_presentation_delay_for_testing(true);
+
permission_controller_ =
std::make_unique<content::MockPermissionController>();
ON_CALL(*permission_controller_, RequestPermissionsFromCurrentDocument)
@@ -110,6 +112,8 @@
}
void ExclusiveAccessTest::TearDownOnMainThread() {
+ ExclusiveAccessBubbleViews::set_skip_presentation_delay_for_testing(false);
+
GetExclusiveAccessManager()
->pointer_lock_controller()
->bubble_hide_callback_for_test_ =
diff --git a/chrome/browser/ui/exclusive_access/fullscreen_controller_interactive_browsertest.cc b/chrome/browser/ui/exclusive_access/fullscreen_controller_interactive_browsertest.cc
index e34c562..0245179 100644
--- a/chrome/browser/ui/exclusive_access/fullscreen_controller_interactive_browsertest.cc
+++ b/chrome/browser/ui/exclusive_access/fullscreen_controller_interactive_browsertest.cc
@@ -80,13 +80,13 @@
} // namespace
class FullscreenControllerInteractiveTest : public ExclusiveAccessTest {
+ protected:
void SetUpOnMainThread() override {
ExclusiveAccessTest::SetUpOnMainThread();
SetDisableFullscreenWithinTab(true);
}
- protected:
void SetUpCommandLine(base::CommandLine* command_line) override {
ExclusiveAccessTest::SetUpCommandLine(command_line);
// Slow bots are flaky due to slower loading interacting with
@@ -981,6 +981,7 @@
}
void SetUpOnMainThread() override {
+ FullscreenControllerInteractiveTest::SetUpOnMainThread();
auto allow_automatic_fullscreen = [&](const GURL& url) {
HostContentSettingsMapFactory::GetForProfile(browser()->profile())
->SetContentSettingDefaultScope(
@@ -1014,7 +1015,10 @@
ASSERT_TRUE(WaitForRenderFrameReady(web_contents_->GetPrimaryMainFrame()));
}
- void TearDownOnMainThread() override { web_contents_ = nullptr; }
+ void TearDownOnMainThread() override {
+ web_contents_ = nullptr;
+ FullscreenControllerInteractiveTest::TearDownOnMainThread();
+ }
bool RequestFullscreen(bool gesture = false,
content::RenderFrameHost* rfh = nullptr) {
@@ -1327,6 +1331,7 @@
: public FullscreenControllerInteractiveTest {
public:
void SetUpOnMainThread() override {
+ FullscreenControllerInteractiveTest::SetUpOnMainThread();
if (!SetUpVirtualDisplays()) {
GTEST_SKIP() << "Skipping test; unavailable multi-screen support.";
}
@@ -1350,6 +1355,7 @@
#if BUILDFLAG(IS_MAC)
ui::NSWindowFakedForTesting::SetEnabled(ns_window_faked_for_testing_);
#endif
+ FullscreenControllerInteractiveTest::TearDownOnMainThread();
}
// Create virtual displays as needed, ensuring 2 displays are available for
@@ -1775,7 +1781,7 @@
// Explicitly check for, and destroy, the exclusive access bubble.
EXPECT_TRUE(IsExclusiveAccessBubbleDisplayed());
- Wait(ExclusiveAccessBubble::kShowTime);
+ Wait(ExclusiveAccessBubble::kShowTime * 2);
FinishExclusiveAccessBubbleAnimation();
EXPECT_FALSE(IsExclusiveAccessBubbleDisplayed());
diff --git a/chrome/browser/ui/views/exclusive_access_bubble_views.cc b/chrome/browser/ui/views/exclusive_access_bubble_views.cc
index 5c0e580..db807b2e 100644
--- a/chrome/browser/ui/views/exclusive_access_bubble_views.cc
+++ b/chrome/browser/ui/views/exclusive_access_bubble_views.cc
@@ -24,10 +24,12 @@
#include "chrome/grit/generated_resources.h"
#include "components/fullscreen_control/fullscreen_features.h"
#include "components/fullscreen_control/subtle_notification_view.h"
+#include "components/viz/common/frame_timing_details.h"
#include "content/public/browser/web_contents.h"
Regression Test / PoC
diff --git a/chrome/browser/ui/exclusive_access/exclusive_access_bubble_unittest.cc b/chrome/browser/ui/exclusive_access/exclusive_access_bubble_unittest.cc
index 5b87012b..a129897 100644
--- a/chrome/browser/ui/exclusive_access/exclusive_access_bubble_unittest.cc
+++ b/chrome/browser/ui/exclusive_access/exclusive_access_bubble_unittest.cc
@@ -13,12 +13,13 @@
explicit MockExclusiveAccessBubble(ExclusiveAccessBubbleParams params)
: ExclusiveAccessBubble(params) {}
~MockExclusiveAccessBubble() override = default;
- MOCK_METHOD(void, Hide, (), (override));
MOCK_METHOD(void, Show, (), (override));
+ MOCK_METHOD(void, Hide, (), (override));
using ExclusiveAccessBubble::hide_timeout_;
using ExclusiveAccessBubble::ShowAndStartTimers;
using ExclusiveAccessBubble::snooze_until_;
+ using ExclusiveAccessBubble::StartHideTimer;
};
class ExclusiveAccessBubbleTest : public testing::Test {
@@ -63,3 +64,20 @@
task_environment_.FastForwardBy(base::Minutes(16));
bubble_.OnUserInput();
}
+
+TEST_F(ExclusiveAccessBubbleTest, StartHideTimerRestartsTimer) {
+ EXPECT_CALL(bubble_, Show()).Times(1);
+ bubble_.ShowAndStartTimers();
+ EXPECT_TRUE(bubble_.hide_timeout_.IsRunning());
+
+ task_environment_.FastForwardBy(base::Seconds(1));
+ auto remaining =
+ bubble_.hide_timeout_.desired_run_time() - base::TimeTicks::Now();
+ EXPECT_LT(remaining, ExclusiveAccessBubble::kShowTime);
+
+ // Verify that the time increases.
+ bubble_.StartHideTimer();
+ auto new_remaining =
+ bubble_.hide_timeout_.desired_run_time() - base::TimeTicks::Now();
+ EXPECT_LT(remaining, new_remaining);
+}
diff --git a/chrome/browser/ui/exclusive_access/exclusive_access_test.cc b/chrome/browser/ui/exclusive_access/exclusive_access_test.cc
index 2756a83..91fd829 100644
--- a/chrome/browser/ui/exclusive_access/exclusive_access_test.cc
+++ b/chrome/browser/ui/exclusive_access/exclusive_access_test.cc
@@ -74,6 +74,8 @@
ExclusiveAccessTest::~ExclusiveAccessTest() = default;
void ExclusiveAccessTest::SetUpOnMainThread() {
+ ExclusiveAccessBubbleViews::set_skip_presentation_delay_for_testing(true);
+
permission_controller_ =
std::make_unique<content::MockPermissionController>();
ON_CALL(*permission_controller_, RequestPermissionsFromCurrentDocument)
@@ -110,6 +112,8 @@
}
void ExclusiveAccessTest::TearDownOnMainThread() {
+ ExclusiveAccessBubbleViews::set_skip_presentation_delay_for_testing(false);
+
GetExclusiveAccessManager()
->pointer_lock_controller()
->bubble_hide_callback_for_test_ =
diff --git a/chrome/browser/ui/exclusive_access/fullscreen_controller_interactive_browsertest.cc b/chrome/browser/ui/exclusive_access/fullscreen_controller_interactive_browsertest.cc
index e34c562..0245179 100644
--- a/chrome/browser/ui/exclusive_access/fullscreen_controller_interactive_browsertest.cc
+++ b/chrome/browser/ui/exclusive_access/fullscreen_controller_interactive_browsertest.cc
@@ -80,13 +80,13 @@
} // namespace
class FullscreenControllerInteractiveTest : public ExclusiveAccessTest {
+ protected:
void SetUpOnMainThread() override {
ExclusiveAccessTest::SetUpOnMainThread();
SetDisableFullscreenWithinTab(true);
}
- protected:
void SetUpCommandLine(base::CommandLine* command_line) override {
ExclusiveAccessTest::SetUpCommandLine(command_line);
// Slow bots are flaky due to slower loading interacting with
@@ -981,6 +981,7 @@
}
void SetUpOnMainThread() override {
+ FullscreenControllerInteractiveTest::SetUpOnMainThread();
auto allow_automatic_fullscreen = [&](const GURL& url) {
HostContentSettingsMapFactory::GetForProfile(browser()->profile())
->SetContentSettingDefaultScope(
@@ -1014,7 +1015,10 @@
ASSERT_TRUE(WaitForRenderFrameReady(web_contents_->GetPrimaryMainFrame()));
}
- void TearDownOnMainThread() override { web_contents_ = nullptr; }
+ void TearDownOnMainThread() override {
+ web_contents_ = nullptr;
+ FullscreenControllerInteractiveTest::TearDownOnMainThread();
+ }
bool RequestFullscreen(bool gesture = false,
content::RenderFrameHost* rfh = nullptr) {
@@ -1327,6 +1331,7 @@
: public FullscreenControllerInteractiveTest {
public:
void SetUpOnMainThread() override {
+ FullscreenControllerInteractiveTest::SetUpOnMainThread();
if (!SetUpVirtualDisplays()) {
GTEST_SKIP() << "Skipping test; unavailable multi-screen support.";
}
@@ -1350,6 +1355,7 @@
#if BUILDFLAG(IS_MAC)
ui::NSWindowFakedForTesting::SetEnabled(ns_window_faked_for_testing_);
#endif
+ FullscreenControllerInteractiveTest::TearDownOnMainThread();
}
// Create virtual displays as needed, ensuring 2 displays are available for
@@ -1775,7 +1781,7 @@
// Explicitly check for, and destroy, the exclusive access bubble.
EXPECT_TRUE(IsExclusiveAccessBubbleDisplayed());
- Wait(ExclusiveAccessBubble::kShowTime);
+ Wait(ExclusiveAccessBubble::kShowTime * 2);
FinishExclusiveAccessBubbleAnimation();
EXPECT_FALSE(IsExclusiveAccessBubbleDisplayed());
Original Bug Report
WebGL Fullscreen Security UI Bypass
VULNERABILITY DETAILS WebGL allows malicious actors to overload GPUs and stall rendering of all Chromium/OS UI elements. Utilizing this, it is possible to obscure the dialog making users aware the browser is entering fullscreen mode (see WHATWG Fullscreen API Living Standard: users should always be informed when something is displayed in fullscreen [1]).
After a user interaction (required to use the fullscreen API), the GPU rendering can be blocked. Meanwhile the browser enters fullscreen. This is not visible to users since any rendering is stalled by resource exhaustion. After rendering has recovered, the browser should be in fullscreen, with the fullscreen notification completely skipped.
Our testing shows that the exploit only works in Windows-based systems, Linux systems are unaffected, as we were not able to reproduce the described behavior in those. This indicates that some Windows-specific implementation may be the reason for this behavior.
The exploit can be utilized for example to create convincing Windows lock-screen look-alike phishing site (if you want a more fleshed-out PoC we can provide a video and/or source code for a full demonstration).
[1] https://fullscreen.spec.whatwg.org/#security-and-privacy-considerations
VERSION Chrome Version: 124.0.6367.208 stable Operating System: Windows 10 Pro, 22H2
REPRODUCTION CASE The attached file ‘minimal_chrome.html’ demonstrates the exploit. It may be required to adjust the STRESS, FULLSCREEN_WAIT and RENDERTIME values, depending on the device. The given values give consistent results when testing on a Lenovo Thinkpad P14s Gen 3 using the integrated Intel Iris Xe Graphics for rendering.
CREDIT INFORMATION Reporter credit: Wolfgang Ettlinger (aff. Certitude Consulting GmbH) Alexander Hurbean (aff. Certitude Consulting GmbH)