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 <[email protected]> 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 <[email protected]> Commit-Queue: Vikram Pasupathy <[email protected]> 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