Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactImproper access control in Ozone
DescriptionImproper access control in Ozone
ComponentOzone
Sub-componentInternals>Ozone
Bug ClassLogic Error
Tracker562237117
Fix commit7d86a2fb002b (chromium/src) +3/-4
CISA KEVNot listed
CreditedGoogle
Disclosed2026-10-20

Changed Functions

FunctionChangeNotes
if
ui/ozone/platform/x11/x11_window.cc
modified

Files Changed

  • ui/ozone/platform/x11/x11_window.cc
From 7d86a2fb002bcc5e123036548dc5aadd76221d06 Mon Sep 17 00:00:00 2001
From: Tom Anderson <thomasanderson@chromium.org>
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 <thomasanderson@chromium.org>
Reviewed-by: Lei Zhang <thestig@chromium.org>
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();
Loading diff…

Original Bug Report

reported by vm...@google.com

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:

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 kSecuritySurface widget generally, e.g. the Autofill popup’s anti-clickjacking z-order under reversed orderings (hardening from BUG:1365884 regressed).

Root Cause Analysis

  1. Global list, creation-ordered; raise loop is the only defense: x11_window.cc:190-193 defines the process-global vector; x11_window.cc:392-402 appends 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 the accept_events = false bubble), and SetZOrderLevel (x11_window.cc:982-991, WM-hint only) provide no compensating re-raise.
  2. The over-erase: x11_window.cc:1964-1969 — the range form erases through end():
    // [`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
    }
    
    With list [A, B] and A closing, B is also removed while its X window stays mapped.
  3. Attacker controls an earlier entry: the datalist/Autofill popup is kSecuritySurface unconditionally 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.
  4. 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 widget Init → push_back(B). The popup’s teardown is always deferred: PopupBaseView::DoHide → Widget::Close() (popup_base_view.cc:373-378) → DesktopWindowTreeHostPlatform::Close() posts CloseNow (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-trial kAsyncFullscreenWindowState flag could alter this ordering; at worst it degrades the attack to a retryable race.)
  5. Cover with a renderer-spawned window: the page’s <select> dropdown becomes a toplevel kMenu override-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

  1. 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).
  2. On the next keydown (transient activation), call document.documentElement.requestFullscreen() — the exit bubble registers as security surface B, after A.
  3. 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.
  4. On the user’s next click, open an attacker-positioned <select> (via showPicker()) whose dropdown covers the top-center of the screen; render a fake fullscreen browser UI.
  5. Expected: the exit bubble, a kSecuritySurface window, 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:

  1. User types in an <input list=...> field → the datalist Autofill popup (an X11 kSecuritySurface window, A) opens. List = [A].
  2. 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].
  3. The page blurs the input (the fullscreen resize does this anyway); A’s widget Close() posts CloseNow → CloseXWindow() runs erase(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.)
  4. 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.
  5. 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). Screenshot attack-3-covered.png shows the fullscreen fake-browser phishing page with no exit notification visible anywhere.
  • stacking-control-3-covered.txt → VERDICT: BUBBLE_ABOVE_MENU. Screenshot control-3-covered.png shows 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 kSecuritySurface widgets 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.

View on issue tracker
Links in the report