CVE-2026-113297
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifui/ozone/platform/x11/x11_window.cc |
modified |
Files Changed
ui/ozone/platform/x11/x11_window.cc
Patch
From 7d86a2fb002bcc5e123036548dc5aadd76221d06 Mon Sep 17 00:00:00 2001 From: Tom Anderson <[email protected]> Date: Wed, 16 Sep 2026 13:34:13 -0700 Subject: [PATCH] Only unregister one window from the X11 security surface list X11Window::CloseXWindow() unregistered a closing security surface with the range overload of vector::erase, which removed the matched window and every security surface registered after it. Since the global list is the only thing that keeps override-redirect security surfaces (the fullscreen exit bubble, the Autofill popup) above renderer-spawned windows, closing an earlier-registered surface silently dropped the protection for all later ones, letting a web page cover the fullscreen exit bubble with a <select> dropdown and spoof browser UI. Use std::erase() so that only the closing window is unregistered. Fixed: 562237117 Change-Id: Id1becca83dcaca1ce21305598b844907b60d52d6 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8415844 Commit-Queue: Thomas Anderson <[email protected]> Reviewed-by: Lei Zhang <[email protected]> Cr-Commit-Position: refs/heads/main@{#1699228} --- diff --git a/ui/ozone/platform/x11/x11_window.cc b/ui/ozone/platform/x11/x11_window.cc index 1947486..bf92fc4 100644 --- a/ui/ozone/platform/x11/x11_window.cc +++ b/ui/ozone/platform/x11/x11_window.cc @@ -5,16 +5,17 @@ #include "ui/ozone/platform/x11/x11_window.h" #include <algorithm> +#include <vector> #include "base/compiler_specific.h" #include "base/memory/scoped_refptr.h" +#include "base/nix/xdg_util.h" #include "base/no_destructor.h" #include "base/notimplemented.h" #include "base/notreached.h" #include "base/strings/string_number_conversions.h" #include "base/task/single_thread_task_runner.h" #include "base/trace_event/trace_event.h" -#include "base/nix/xdg_util.h" #include "net/base/network_interfaces.h" #include "third_party/skia/include/core/SkPath.h" #include "third_party/skia/include/core/SkRegion.h" @@ -1962,9 +1963,7 @@ UnconfineCursor(); // Unregister from the global security surface list if necessary. if (is_security_surface_) { - auto& security_surfaces = GetSecuritySurfaces(); - security_surfaces.erase(std::ranges::find(security_surfaces, xwindow_), - security_surfaces.end()); + std::erase(GetSecuritySurfaces(), xwindow_); } geometry_cache_.reset();
Original Bug Report
Fullscreen bubble deprotection via range erase in X11Window::CloseXWindow
Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: X11Window::CloseXWindow() unregisters a closing security surface from the global X11 security-surface list using the range overload of vector::erase, deleting the matched window and every security surface registered after it. A web page can trigger this deterministically on Linux/X11: it opens the datalist Autofill popup (an earlier-registered security surface), requests fullscreen (registering the “Press Esc to exit full screen” bubble after it), then lets the popup close — silently dropping the bubble from the raise list. Any renderer-spawned override-redirect window created afterwards (<select> dropdown, tooltip, drag image) then stacks above the still-mapped bubble and is never restacked below it, letting the page hide Chromium’s only fullscreen-phishing defense and render a fake browser UI.
Affected files:
ui/ozone/platform/x11/x11_window.cc:1964-1969— range erase removes all later-registered security surfaces, not just the closing oneui/ozone/platform/x11/x11_window.cc:190-193— process-global, creation-orderedGetSecuritySurfaces()vectorui/ozone/platform/x11/x11_window.cc:392-402— registration plus the raise loop that is the sole stacking protection for override-redirect security surfacescomponents/fullscreen_control/subtle_notification_view.cc:268— fullscreen exit bubble registers askSecuritySurfacechrome/browser/ui/views/autofill/popup/popup_base_view.cc:130-132— Autofill/datalist popup registers askSecuritySurfaceunconditionally on Linux (the attacker-controlled earlier list entry)
Estimated timestamp from git blame: 2022-06-14
Summary
On Linux/X11, windows with ui::ZOrderLevel::kSecuritySurface (the fullscreen/pointer-lock exit bubble, the Autofill popup) are kept above web-content popups by exactly one mechanism: a global vector of X windows that is re-raised whenever any non-security X window is created (ui/ozone/platform/x11/x11_window.cc:399-401). Both the bubble and the covering windows (kPopup, kMenu, kTooltip, kDrag) are override-redirect (x11_window.cc:1871-1885), so no window manager ever restacks them — the list is the whole defense, on any WM.
CloseXWindow() is meant to remove one entry from that list but erases the range [find(xwindow_), end()). Closing an earlier-registered surface therefore silently unregisters every later one. When find misses, erase(end(), end()) is a no-op, so the bug never crashes — it only strips protection.
Consequences, demonstrated end-to-end by an executed reproduction (attached):
- A plain web page (default flags, no compromised renderer, interaction limited to typing two characters and one click) enters HTML fullscreen while ensuring the exit bubble is covered by an attacker-positioned
<select>dropdown. - Suppression persists for the interactive session: the bubble shows for 3.8 s and re-shows only after 15 input-free minutes (
chrome/browser/ui/exclusive_access/exclusive_access_bubble.h:32-34), every input re-arms the snooze, re-mapping never restacks (x11_window.cc:2018), and each new popup re-covers it via the now-empty raise list. - The page can then render a pixel-perfect fake browser frame (fake omnibox/TLS UI/login form) — exactly the attack the bubble exists to prevent per Chromium’s own comment: “This bubble is the only defense that Chromium can provide against this attack” (
chrome/browser/ui/views/exclusive_access/exclusive_access_bubble_views.cc:124-133). - The same over-erase deprotects any later-registered
kSecuritySurfacewidget generally, e.g. the Autofill popup’s anti-clickjacking z-order under reversed orderings (hardening from BUG:1365884 regressed).
Root Cause Analysis
- Global list, creation-ordered; raise loop is the only defense:
x11_window.cc:190-193defines the process-global vector;x11_window.cc:392-402appends security surfaces at init and, for every non-security window created, raises all listed surfaces — because, as the comment notes, the WM will not restack override-redirect windows. These are the only writers/readers besides the buggy erase;Map(x11_window.cc:2018),Activate(unreachable for theaccept_events = falsebubble), andSetZOrderLevel(x11_window.cc:982-991, WM-hint only) provide no compensating re-raise. - The over-erase:
x11_window.cc:1964-1969— the range form erases throughend():With list// [`ui/ozone/platform/x11/x11_window.cc:1964-1969`](https://source.chromium.org/chromium/chromium/src/+/main:ui/ozone/platform/x11/x11_window.cc;l=1964) if (is_security_surface_) { auto& security_surfaces = GetSecuritySurfaces(); security_surfaces.erase(std::ranges::find(security_surfaces, xwindow_), security_surfaces.end()); // <-- erases [match, end), not one element }[A, B]and A closing, B is also removed while its X window stays mapped. - Attacker controls an earlier entry: the datalist/Autofill popup is
kSecuritySurfaceunconditionally on Linux (popup_base_view.cc:130-132) and needs no stored user data — attacker-supplied<datalist>options suffice (chrome/browser/ui/autofill/autofill_popup_controller_impl.cc:313-316). It becomes list entry A. - Bubble registration always precedes the erase:
requestFullscreen()runs one synchronous UI-task chain —FullscreenController::EnterFullscreenModeInternal→BrowserView::ProcessFullscreen(chrome/browser/ui/views/frame/browser_view.cc:5500-5519; X11 sets fullscreen state synchronously,x11_window.cc:698-713) →FullscreenStateChanged→UpdateExclusiveAccessBubble→ bubble widgetInit→push_back(B). The popup’s teardown is always deferred:PopupBaseView::DoHide→Widget::Close()(popup_base_view.cc:373-378) →DesktopWindowTreeHostPlatform::Close()postsCloseNow(ui/views/widget/desktop_aura/desktop_window_tree_host_platform.cc:409-411). So[A, B]before the erase in every interleaving; step 4’s erase then empties the list. (Only the non-default, no-field-trialkAsyncFullscreenWindowStateflag could alter this ordering; at worst it degrades the attack to a retryable race.) - Cover with a renderer-spawned window: the page’s
<select>dropdown becomes a toplevelkMenuoverride-redirect X window created atop all siblings (content/browser/renderer_host/render_widget_host_view_aura.cc:452-480,ui/views/widget/desktop_aura/desktop_native_widget_aura.cc:249-273). Its init runs the raise loop over the empty list, so the bubble stays underneath.
Reproduction
- On Linux/X11 (any WM, default flags), serve a page with
<input list=s>plus an attacker-populated<datalist id=s>(framed e.g. as a typing game). User focuses the field and types — the datalist Autofill popup appears (security surface A registered). - On the next keydown (transient activation), call
document.documentElement.requestFullscreen()— the exit bubble registers as security surface B, after A. - Blur the input (the fullscreen resize also does this) — A’s widget closes via a posted task;
CloseXWindow()erases A and B from the list. - On the user’s next click, open an attacker-positioned
<select>(viashowPicker()) whose dropdown covers the top-center of the screen; render a fake fullscreen browser UI. - Expected: the exit bubble, a
kSecuritySurfacewindow, is raised above the dropdown so the user sees “To exit full screen, press Esc”.
Observed (executed reproduction on a vulnerable Linux x64 build under openbox): the X11 stacking order shows the dropdown menu window stacked directly above the still-mapped bubble and fully covering its geometry — no exit notification visible over the fake sign-in page. A control run identical except that popup A is never created shows the bubble raised above the same dropdown, and a build with a single-element erase keeps the bubble on top under the identical choreography — isolating the range erase as the sole cause. The bubble may be briefly glimpsed (sub-second) between fullscreen entry and the covering click; the attacker controls that gap via the page’s interaction cadence, and after the cover, suppression is deterministic for the session.
Severity
medium — Browser-process logic defect reachable from ordinary web content on default-configuration Linux/X11 with mundane interaction (typing in a form field plus one click); no memory corruption. Chrome’s triage record for this exact class — obscuring/suppressing the fullscreen exit notification enabling UI spoofing — is consistently Medium (CVE-2024-13178, CVE-2025-0440, CVE-2023-4365, CVE-2024-6995; CVE-2018-6096 is the same shape). This instance sits at the top of that band rather than the generic S3 “draws over browser UI” floor because it fully disables the kSecuritySurface raise-list mechanism for all later-registered surfaces, is deterministic rather than racy, and persists for the whole interactive session via the input-re-armed 15-minute snooze.
Suggested Fix Direction
Replace the range erase with a single-element erase, e.g. std::erase(security_surfaces, xwindow_); in X11Window::CloseXWindow().
POC: X11Window::CloseXWindow erase(find, end) over-erase deprotects all later-registered security surfaces — web page deterministically drops the fullscreen “Press Esc” bubble from the raise list and covers it (fullscreen phishing)
Strategy: web-content
Bug Type: logic error / UI-spoof enabler (CWE-451) — vector::erase(range) misuse in
ui/ozone/platform/x11/x11_window.cc (X11Window::CloseXWindow)
Summary
X11Window::CloseXWindow() unregisters a closing security surface with the
range overload of vector::erase:
security_surfaces.erase(std::ranges::find(security_surfaces, xwindow_),
security_surfaces.end()); // erases [match, end)!
This deletes the matched window and every security surface registered after
it. The global list is the only mechanism that keeps X11 security surfaces
(fullscreen exit bubble, Autofill popup) above renderer-spawned
override-redirect windows: whenever any non-security X window is created,
Chromium raises every listed surface above it (x11_window.cc:399). Both the
bubble and covering menus/tooltips are override-redirect, so no window manager
ever restacks them — once dropped from the list, the bubble can be covered by
any freshly-created popup and nothing ever raises it again.
A plain web page exploits this deterministically:
- User types in an
<input list=...>field → the datalist Autofill popup (an X11kSecuritySurfacewindow, A) opens. List =[A]. - On the next keystroke the page calls
requestFullscreen(). The exit bubble (“To exit full screen, press Esc”, B) is created synchronously inside the fullscreen-request task → list =[A, B]. - The page blurs the input (the fullscreen resize does this anyway); A’s
widget
Close()postsCloseNow→CloseXWindow()runserase(find(A), end())→ A and B are both removed. B stays mapped but is no longer raise-protected. (B’s registration always precedes the erase: step 2 is same-task-synchronous, step 3 is post-task-deferred.) - The user’s next ordinary click opens a page-positioned
<select>dropdown (showPicker()), a new override-redirect menu X window created on top of all siblings. Its init runs the raise loop over the empty list — the exit bubble stays underneath, invisible. - The page renders a pixel-perfect fake browser UI in fullscreen (fake
omnibox showing
https://accounts.google.com, fake sign-in form). The bubble re-shows only after 15 input-free minutes (kSnoozeTime), and any re-shown/new window repeats step 4 — the warning is suppressed for the whole interactive session.
Total user interaction: typing two characters in a form field + one click — normal cadence for the “typing game” framing the page presents.
Prerequisites
- Linux, X11 display with any window manager (verified with openbox; the mechanism is WM-independent since all involved windows are override-redirect).
- Chromium build at
/chromium_src/src/out/asan/chrome(any Linux/X11 build works; this is a logic bug — no sanitizer needed, the oracle is the X11 stacking order). python3,xdotool,scrot.
Reproduction
cd /poc-artifacts
./run_poc.sh attack # trigger the bug -> VERDICT: MENU_ABOVE_BUBBLE
./run_poc.sh control # baseline (no bug path) -> VERDICT: BUBBLE_ABOVE_MENU
run_poc.sh serves the page, launches Chrome headful, and plays the user
role with xdotool: types a (opens datalist popup A), types b
(page requests fullscreen → bubble B; page then blurs the input → A closes →
over-erase), clicks once (page opens the covering <select> dropdown). After
each step stack_probe.py dumps all viewable root X windows in bottom-to-top
stacking order (XQueryTree) and classifies the bubble and the menu; scrot
captures screenshots.
The control run is identical except no datalist popup is ever created, so nothing is erased from the list: the raise loop then correctly restacks the bubble above the freshly-created dropdown. This isolates the over-erase as the only difference.
Environment notes: --no-proxy-server (sandbox proxy would break localhost),
--ignore-gpu-blocklist (software-GL sandbox), --no-first-run --no-default-browser-check --user-data-dir (fresh profile). None affect the
bug path.
Expected Result
stacking-attack-3-covered.txt→VERDICT: MENU_ABOVE_BUBBLE— the renderer-spawned dropdown is stacked above the exit bubble and its geometry (960x482 at y=26) fully covers the bubble (504x51 at y=45). Screenshotattack-3-covered.pngshows the fullscreen fake-browser phishing page with no exit notification visible anywhere.stacking-control-3-covered.txt→VERDICT: BUBBLE_ABOVE_MENU. Screenshotcontrol-3-covered.pngshows the bubble drawn on top of the same dropdown.
Output
Attack run (vulnerable build), final stacking probe
(stacking-attack-3-covered.txt):
# viewable root children, bottom-to-top stacking order (higher index = closer to the user)
# idx window geometry (x,y WxH) OR class | name
1 0x00200261 ( 0, 0 1920x1200) 0 |
11 0x0020020b ( -100,-100 1x1 ) 1 / |
16 0x0040000c ( 708, 45 504x51 ) 1 |
17 0x0040001a ( 480, 26 960x482 ) 1 |
BUBBLE 0x0040000c stack_index=16 geom=(708,45 504x51)
MENU 0x0040001a stack_index=17 geom=(480,26 960x482)
VERDICT: MENU_ABOVE_BUBBLE (exit bubble is COVERED -- vulnerability triggered)
Control run (same build, no security surface destroyed), final probe
(stacking-control-3-covered.txt):
BUBBLE 0x00400008 stack_index=17 geom=(708,45 504x51)
MENU 0x00400015 stack_index=16 geom=(480,26 960x482)
VERDICT: BUBBLE_ABOVE_MENU (raise loop protected the bubble)
Bandaid Fix (verified)
bandaid-fix.patch replaces the range erase with a single-element erase:
std::erase(GetSecuritySurfaces(), xwindow_);
Verification: with the patch applied and autoninja -C out/asan chrome
rebuilt, the identical attack choreography (./run_poc.sh fixed) yields
VERDICT: BUBBLE_ABOVE_MENU (stacking-fixed-3-covered.txt,
fixed-3-covered.png — the bubble renders on top of the covering dropdown),
proving the over-erase at x11_window.cc:1967 is the root cause. The patch
was reverted after verification; no Chromium source modification is needed to
trigger the bug.
Artifact Index
| File | What it shows |
|---|---|
poc.html |
attack page (datalist + fullscreen + blur + covering <select> + fake sign-in UI) |
control.html |
baseline page (no datalist popup ever created) |
server.py, run_poc.sh, stack_probe.py |
HTTP server, choreography driver, X11 stacking oracle |
attack-1-popupA-open.png / stacking-attack-1-*.txt |
security surface A (datalist popup, 164x238 OR window) open |
attack-2-fullscreen.png / stacking-attack-2-*.txt |
fullscreen entered, A destroyed (over-erase already happened), bubble mapped at (708,45 504x51) |
attack-3-covered.png / stacking-attack-3-*.txt |
dropdown covers the bubble; fake Google sign-in fullscreen; MENU_ABOVE_BUBBLE |
control-2/3-*.png, stacking-control-*.txt |
same cover attempt without the bug → BUBBLE_ABOVE_MENU |
fixed-3-covered.png, stacking-fixed-3-covered.txt |
attack choreography on patched build → BUBBLE_ABOVE_MENU |
bandaid-fix.patch |
one-line fix in X11Window::CloseXWindow |
Notes
- The bubble is briefly visible between fullscreen entry and the user’s next
click (see
attack-2-fullscreen.png); the attacker controls that gap via the page’s interaction cadence. Once covered, suppression persists for the session: re-show requires 15 input-free minutes, re-mapping never restacks, and every new popup window re-covers via the empty raise list. - The same over-erase strips any security surface registered after the
destroyed one (e.g. the Autofill popup’s anti-clickjacking z-order in
reversed orderings) — one defect, all X11
kSecuritySurfacewidgets affected.
Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:
- If you are familiar with the severity guidelines, you may adjust the severity.
- If this is a false positive, and there’s no work to be done, please close as WAI.
- If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.
Data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.
- https://accounts.google.com
- https://source.chromium.org/chromium/chromium/src/+/main:chrome/browser/ui/autofill/autofill_popup_controller_impl.cc;l=313
- https://source.chromium.org/chromium/chromium/src/+/main:chrome/browser/ui/exclusive_access/exclusive_access_bubble.h;l=32
- https://source.chromium.org/chromium/chromium/src/+/main:chrome/browser/ui/views/autofill/popup/popup_base_view.cc;l=130
- https://source.chromium.org/chromium/chromium/src/+/main:chrome/browser/ui/views/exclusive_access/exclusive_access_bubble_views.cc;l=124
- https://source.chromium.org/chromium/chromium/src/+/main:chrome/browser/ui/views/frame/browser_view.cc;l=5500
- https://source.chromium.org/chromium/chromium/src/+/main:components/fullscreen_control/subtle_notification_view.cc;l=268
- https://source.chromium.org/chromium/chromium/src/+/main:content/browser/renderer_host/render_widget_host_view_aura.cc;l=452
- https://source.chromium.org/chromium/chromium/src/+/main:ui/ozone/platform/x11/x11_window.cc
- https://source.chromium.org/chromium/chromium/src/+/main:ui/ozone/platform/x11/x11_window.cc;l=190
- https://source.chromium.org/chromium/chromium/src/+/main:ui/ozone/platform/x11/x11_window.cc;l=1964
- https://source.chromium.org/chromium/chromium/src/+/main:ui/ozone/platform/x11/x11_window.cc;l=392
- https://source.chromium.org/chromium/chromium/src/+/main:ui/ozone/platform/x11/x11_window.cc;l=399
- https://source.chromium.org/chromium/chromium/src/+/main:ui/views/widget/desktop_aura/desktop_native_widget_aura.cc;l=249
- https://source.chromium.org/chromium/chromium/src/+/main:ui/views/widget/desktop_aura/desktop_window_tree_host_platform.cc;l=409