Chrome · FullScreen › Chrome
CVE-2026-113291
Memory Corruption in FullScreen (Chrome)
Overview
Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchrome/browser/ui/android/exclusive_access/exclusive_access_bubble_android.cc |
modified | |
ifchrome/browser/ui/views/exclusive_access/exclusive_access_bubble_views.cc |
modified |
Files Changed
chrome/browser/ui/android/exclusive_access/exclusive_access_bubble_android_unittest.ccchrome/browser/ui/views/exclusive_access/exclusive_access_bubble_views.ccchrome/browser/ui/android/exclusive_access/exclusive_access_bubble_android.cc
Patch
From dc374dc36249b531b9a5d3b59e8574519781c0cf Mon Sep 17 00:00:00 2001
From: Vikram Pasupathy <vpasupathy@chromium.org>
Date: Mon, 21 Sep 2026 12:52:43 -0700
Subject: [PATCH] chrome: Unlatch download override notification on fullscreen exit update
When a download begins while in fullscreen, notify_overridden_ was
previously sticky and kept params_.has_download pinned to true for all
subsequent updates and re-shows. Because GetInstructionTextForType()
omits the origin whenever has_download is true, the site origin was
permanently suppressed after any download occurred during fullscreen.
This CL unlatches notify_overridden_ when !params.has_download or when
the origin changes, ensuring the origin is restored once the download
notification clears.
Bug: 514008724
Change-Id: I9967c259dba339095464a7d6ed1e2ab2fd12b9a5
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8429630
Reviewed-by: Foromo Daniel Soromou <koretadaniel@chromium.org>
Commit-Queue: Vikram Pasupathy <vpasupathy@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1701980}
---
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 dccce5e6..322a4b5 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
@@ -123,7 +123,8 @@
params.has_download);
bool already_shown = IsVisible();
if (params_.type == params.type && params_.origin == params.origin &&
- !params.force_update && (already_shown || was_shown_)) {
+ params_.has_download == params.has_download && !params.force_update &&
+ (already_shown || was_shown_)) {
return;
}
was_shown_ = true;
@@ -137,9 +138,13 @@
// or the previous notification was about an override itself.
// If both the previous and current notifications have a download, but
// neither is an override, then we don't need to show an override.
- notify_overridden_ =
- already_shown &&
- (notify_overridden_ || (params.has_download ^ params_.has_download));
+ if (!params.has_download && !params.origin.opaque()) {
+ notify_overridden_ = false;
+ } else {
+ notify_overridden_ =
+ already_shown &&
+ (notify_overridden_ || (params.has_download ^ params_.has_download));
+ }
params_.has_download = params.has_download || notify_overridden_;
// Bubble maybe be reused after timeout.
@@ -147,10 +152,10 @@
first_hide_callback_ = std::move(first_hide_callback);
- params_.origin = params.origin;
- // When a request to notify about a download is made, the bubble type
- // should be preserved from the old value, and not be updated.
+ // When a request to notify about a download is made, the bubble type and
+ // origin should be preserved from the old value, and not be updated.
if (!params.has_download) {
+ params_.origin = params.origin;
params_.type = params.type;
}
UpdateBubbleContent(params_.type);
diff --git a/chrome/browser/ui/views/exclusive_access/exclusive_access_bubble_views.cc b/chrome/browser/ui/views/exclusive_access/exclusive_access_bubble_views.cc
index f4f21de..3df2459 100644
--- a/chrome/browser/ui/views/exclusive_access/exclusive_access_bubble_views.cc
+++ b/chrome/browser/ui/views/exclusive_access/exclusive_access_bubble_views.cc
@@ -199,7 +199,8 @@
params.has_download);
bool already_shown = IsShowing() || IsVisible();
if (params_.type == params.type && params_.origin == params.origin &&
- !params.force_update && already_shown) {
+ params_.has_download == params.has_download && !params.force_update &&
+ already_shown) {
return;
}
@@ -209,9 +210,13 @@
// or the previous notification was about an override itself.
// If both the previous and current notifications have a download, but
// neither is an override, then we don't need to show an override.
- notify_overridden_ =
- already_shown &&
- (notify_overridden_ || (params.has_download ^ params_.has_download));
+ if (!params.has_download && !params.origin.opaque()) {
+ notify_overridden_ = false;
+ } else {
+ notify_overridden_ =
+ already_shown &&
+ (notify_overridden_ || (params.has_download ^ params_.has_download));
+ }
params_.has_download = params.has_download || notify_overridden_;
// Bubble maybe be re-used after timeout.
@@ -222,10 +227,10 @@
const bool entering_tab_fullscreen =
!IsTabFullscreenType(params_.type) && IsTabFullscreenType(params.type);
- params_.origin = params.origin;
- // When a request to notify about a download is made, the bubble type
- // should be preserved from the old value, and not be updated.
+ // When a request to notify about a download is made, the bubble type and
+ // origin should be preserved from the old value, and not be updated.
if (!params.has_download) {
+ params_.origin = params.origin;
params_.type = params.type;
}
UpdateViewContent(params_.type);
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 2be564f..330f55c 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
@@ -12,6 +12,7 @@
#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 "components/url_formatter/elide_url.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/base/l10n/l10n_util.h"
@@ -260,4 +261,55 @@
testing::Mock::VerifyAndClearExpectations(mock_bridge_ptr);
}
+TEST_F(ExclusiveAccessBubbleAndroidTest,
+ DownloadCompletionRestoresOriginInNotice) {
+ url::Origin origin = url::Origin::Create(GURL("https://example.com"));
+ auto format_text = [&](bool has_download) {
+ return exclusive_access_bubble::GetInstructionTextForTypeTouchBased(
+ EXCLUSIVE_ACCESS_BUBBLE_TYPE_FULLSCREEN_EXIT_INSTRUCTION,
+ url_formatter::FormatOriginForSecurityDisplay(
+ origin, url_formatter::SchemeDisplay::OMIT_CRYPTOGRAPHIC),
+ has_download, /*notify_overridden=*/has_download);
+ };
+
+ ExclusiveAccessBubbleParams params;
+ params.type = EXCLUSIVE_ACCESS_BUBBLE_TYPE_FULLSCREEN_EXIT_INSTRUCTION;
+ params.origin = origin;
+
+ auto mock_bridge = std::make_unique<MockBridge>();
+ auto* mock_bridge_ptr = mock_bridge.get();
+ EXPECT_CALL(*mock_bridge_ptr, IsKeyboardConnected())
+ .WillRepeatedly(Return(false));
+
+ EXPECT_CALL(*mock_bridge_ptr, IsVisible()).WillOnce(Return(false));
+ EXPECT_CALL(*mock_bridge_ptr, Update(format_text(false))).Times(1);
+ EXPECT_CALL(*mock_bridge_ptr, Show()).Times(1);
+ ExclusiveAccessBubbleAndroid bubble(params, base::DoNothing(),
+ std::move(mock_bridge));
+
+ // Download starts while in fullscreen (DownloadDisplayController passes an
+ // empty/opaque origin), overriding the notice.
+ params.type = EXCLUSIVE_ACCESS_BUBBLE_TYPE_NONE;
+ params.origin = url::Origin();
+ params.has_download = true;
+ params.force_update = true;
+ EXPECT_CALL(*mock_bridge_ptr, IsVisible()).WillOnce(Return(true));
+ EXPECT_CALL(*mock_bridge_ptr, Update(format_text(true))).Times(1);
+ EXPECT_CALL(*mock_bridge_ptr, Show()).Times(1);
+ bubble.Update(params, base::DoNothing());
+ EXPECT_TRUE(bubble.params().has_download);
+
+ // Subsequent update with a non-opaque origin unlatches override and restores
+ // the origin in the notice.
+ params.type = EXCLUSIVE_ACCESS_BUBBLE_TYPE_FULLSCREEN_EXIT_INSTRUCTION;
+ params.origin = origin;
+ params.has_download = false;
+ params.force_update = false;
+ EXPECT_CALL(*mock_bridge_ptr, IsVisible()).WillOnce(Return(true));
+ EXPECT_CALL(*mock_bridge_ptr, Update(format_text(false))).Times(1);
+ EXPECT_CALL(*mock_bridge_ptr, Show()).Times(1);
+ bubble.Update(params, base::DoNothing());
+ EXPECT_FALSE(bubble.params().has_download);
+}
+
} // namespace
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 2be564f..330f55c 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
@@ -12,6 +12,7 @@
#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 "components/url_formatter/elide_url.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/base/l10n/l10n_util.h"
@@ -260,4 +261,55 @@
testing::Mock::VerifyAndClearExpectations(mock_bridge_ptr);
}
+TEST_F(ExclusiveAccessBubbleAndroidTest,
+ DownloadCompletionRestoresOriginInNotice) {
+ url::Origin origin = url::Origin::Create(GURL("https://example.com"));
+ auto format_text = [&](bool has_download) {
+ return exclusive_access_bubble::GetInstructionTextForTypeTouchBased(
+ EXCLUSIVE_ACCESS_BUBBLE_TYPE_FULLSCREEN_EXIT_INSTRUCTION,
+ url_formatter::FormatOriginForSecurityDisplay(
+ origin, url_formatter::SchemeDisplay::OMIT_CRYPTOGRAPHIC),
+ has_download, /*notify_overridden=*/has_download);
+ };
+
+ ExclusiveAccessBubbleParams params;
+ params.type = EXCLUSIVE_ACCESS_BUBBLE_TYPE_FULLSCREEN_EXIT_INSTRUCTION;
+ params.origin = origin;
+
+ auto mock_bridge = std::make_unique<MockBridge>();
+ auto* mock_bridge_ptr = mock_bridge.get();
+ EXPECT_CALL(*mock_bridge_ptr, IsKeyboardConnected())
+ .WillRepeatedly(Return(false));
+
+ EXPECT_CALL(*mock_bridge_ptr, IsVisible()).WillOnce(Return(false));
+ EXPECT_CALL(*mock_bridge_ptr, Update(format_text(false))).Times(1);
+ EXPECT_CALL(*mock_bridge_ptr, Show()).Times(1);
+ ExclusiveAccessBubbleAndroid bubble(params, base::DoNothing(),
+ std::move(mock_bridge));
+
+ // Download starts while in fullscreen (DownloadDisplayController passes an
+ // empty/opaque origin), overriding the notice.
+ params.type = EXCLUSIVE_ACCESS_BUBBLE_TYPE_NONE;
+ params.origin = url::Origin();
+ params.has_download = true;
+ params.force_update = true;
+ EXPECT_CALL(*mock_bridge_ptr, IsVisible()).WillOnce(Return(true));
+ EXPECT_CALL(*mock_bridge_ptr, Update(format_text(true))).Times(1);
+ EXPECT_CALL(*mock_bridge_ptr, Show()).Times(1);
+ bubble.Update(params, base::DoNothing());
+ EXPECT_TRUE(bubble.params().has_download);
+
+ // Subsequent update with a non-opaque origin unlatches override and restores
+ // the origin in the notice.
+ params.type = EXCLUSIVE_ACCESS_BUBBLE_TYPE_FULLSCREEN_EXIT_INSTRUCTION;
+ params.origin = origin;
+ params.has_download = false;
+ params.force_update = false;
+ EXPECT_CALL(*mock_bridge_ptr, IsVisible()).WillOnce(Return(true));
+ EXPECT_CALL(*mock_bridge_ptr, Update(format_text(false))).Times(1);
+ EXPECT_CALL(*mock_bridge_ptr, Show()).Times(1);
+ bubble.Update(params, base::DoNothing());
+ EXPECT_FALSE(bubble.params().has_download);
+}
+
} // 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.
References
On This Page