CVE-2026-102314
Overview
Background
- `TabDialogManager` (TDM)
- the object that hosts a tab-modal dialog widget for a tab and manages the input-blocking state of that tab’s web contents while the dialog is up.
- `SetWebContentsBlocked`
- a capability method that toggles whether a tab’s web contents accepts input; blocking it is how a modal dialog prevents the page underneath from being interacted with.
- `params_->disable_input`
- the flag recording whether this dialog instance requested that the tab’s web contents be input-disabled when the dialog was shown.
- Legacy dialog
- an older, non-TDM dialog path that can independently set the same web-contents-blocked bit on the same tab.
Root Cause Analysis
When a tab-modal dialog widget was destroyed, TabDialogManager::WidgetDestroyed unconditionally called SetWebContentsBlocked(..., /*blocked=*/false), re-enabling input on the tab’s web contents. This assumed the TDM was always the party that had blocked the contents, but the blocked bit is shared: a concurrent legacy dialog could have set it while the TDM’s own dialog did not request input disabling (params_->disable_input false). In that case the TDM’s teardown blindly clobbered the shared state, unblocking web contents that another still-present dialog expected to remain blocked, an unbalanced state reset.
The fix captures did_block_web_contents = params_ && params_->disable_input before resetting params_, and only clears the blocked bit when the TDM had actually set it, so foreign dialog state is left intact.
did_block_web_contents so the TDM only undoes the blocking it itself performed.Attack Path
- Establish a legacy-dialog block
A page or flow triggers a legacy (non-TDM) dialog that calls
SetWebContentsBlocked(true)on the target tab, correctly disabling input on the underlying web contents. - Layer a non-disabling TDM dialog
A second, TDM-managed dialog is shown on the same tab whose
params_->disable_inputis false, so the TDM never touches the blocked bit on show. (inferred) - Dismiss the TDM dialog
When that TDM widget is destroyed,
WidgetDestroyedruns and, pre-patch, unconditionally callsSetWebContentsBlocked(false), clearing the block the legacy dialog still relies on. - Interact past a live dialog The web contents is now input-enabled while a modal dialog remains on screen, letting the page underneath be interacted with / mispresented, i.e. UI misrepresentation in the TabStrip per the advisory.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchrome/browser/ui/tabs/tab_dialog_manager.cc |
modified |
Files Changed
chrome/browser/ui/tabs/tab_dialog_manager.cc
Audit Directions
- Shared boolean/flag ownershipLook for teardown paths that unconditionally reset a shared UI or security state bit without first confirming this object was the one that set it.
- Modality lifecycle racesAudit places where multiple dialog implementations (legacy vs. new manager) can co-exist on one tab/window and both mutate the same input-blocking or visibility state.
- Capabilities()->Set... resetsReview other callers of
capabilities()->SetWebContentsBlockedand similar toggles for balanced set/clear pairing tied to per-instance intent rather than a global assumption.
Patch
From 7f0f9b9fdd518e523072fabe73f6d95a4692e63b Mon Sep 17 00:00:00 2001
From: Allen Bauer <kylixrd@chromium.org>
Date: Thu, 27 Aug 2026 12:13:19 -0700
Subject: [PATCH] [TabDialogManager] Only re-enable web contents if TDM had disabled it.
This fixes an unbalanced state reset if the TDM is handling a dialog
which did not disable web contents while another legacy dialog did.
In that case, the TDM shouldn't blindly blast over the state controlling
the web contents.
Bug: 514059780
Change-Id: Iae1808523960bf874591841b74289626e9348bfa
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8307096
Reviewed-by: Keren Zhu <kerenzhu@chromium.org>
Commit-Queue: Allen Bauer <kylixrd@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1687466}
---
diff --git a/chrome/browser/ui/tabs/tab_dialog_manager.cc b/chrome/browser/ui/tabs/tab_dialog_manager.cc
index 7d55c06..dcbcd60 100644
--- a/chrome/browser/ui/tabs/tab_dialog_manager.cc
+++ b/chrome/browser/ui/tabs/tab_dialog_manager.cc
@@ -405,15 +405,23 @@
void TabDialogManager::WidgetDestroyed(views::Widget* widget) {
CHECK_EQ(widget, widget_.get());
+ // Check if we had blocked the web contents in this iteration.
+ const bool did_block_web_contents = params_ && params_->disable_input;
widget_ = nullptr;
params_.reset();
tab_dialog_widget_observer_.reset();
scoped_ignore_input_events_.reset();
web_contents_modal_dialog_host_observer_.reset();
bounds_animation_.reset();
- tab_interface_->GetBrowserWindowInterface()
- ->capabilities()
- ->SetWebContentsBlocked(tab_interface_->GetContents(), /*blocked=*/false);
+ // Only clobber the blocked web contents bit if we were also the one that
+ // blocked it. Otherwise leave it alone, since it may have been set by some
+ // other legacy dialog.
+ if (did_block_web_contents) {
+ tab_interface_->GetBrowserWindowInterface()
+ ->capabilities()
+ ->SetWebContentsBlocked(tab_interface_->GetContents(),
+ /*blocked=*/false);
+ }
// Resetting ScopedTabModalUI may cause the showing of a new dialog.
// Leaving it at the end of the function to prevent its side effects
// from being overridden.