CVE-2026-19177
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
TESTui/base/cursor/cursor_unittest.cc |
modified |
Files Changed
ui/base/cursor/cursor_factory.ccui/base/cursor/cursor_factory.hui/base/cursor/cursor_unittest.ccui/base/x/x11_cursor_loader.ccui/gtk/gtk_ui.cc
Patch
From 128dd53d2480f175488af33fdf13d16e878f5748 Mon Sep 17 00:00:00 2001
From: Tom Anderson <thomasanderson@chromium.org>
Date: Fri, 14 Aug 2026 20:54:45 -0700
Subject: [PATCH] Reland "Reland "Sanitize cursor theme name and size in GTK and Wayland""
This is a reland of commit 91a2334cbf479f688cc421aa7f7eb1802e8e2bfb
Original change's description:
> Reland "Sanitize cursor theme name and size in GTK and Wayland"
>
> This is a reland of commit 283ac1ef12df16eb0256a4d5ce83477b5afba061
>
> Original change's description:
> > Sanitize cursor theme name and size in GTK and Wayland
> >
> > This change extends GtkSettingsSetProperty interceptor and GtkUi to
> > sanitize gtk-cursor-theme-name and gtk-cursor-theme-size at write-time,
> > preventing unsanitized cursor theme names from being set in GTK
> > settings.
> >
> > It also introduces unified ui::IsValidCursorThemeName and
> > ui::IsValidCursorThemeSize validation helpers in //ui/base/cursor, and
> > adds validation to WaylandCursorFactory to ensure invalid or
> > path-traversing cursor theme names are rejected before calling
> > wl_cursor_theme_load.
> >
> > Change-Id: Ibe8a3ce208d76d2e724902424491a6116d4352f6
> > Fixed: 540289900
> > Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8164559
> > Reviewed-by: Yichen Zhou <yichenz@chromium.org>
> > Commit-Queue: Thomas Anderson <thomasanderson@chromium.org>
> > Cr-Commit-Position: refs/heads/main@{#1670761}
>
> Bug: 542165218
> Fixed: 540289900
> Change-Id: I29b295e3296eb82a1304d9884d547cf939528f5a
> Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8180111
> Reviewed-by: Yichen Zhou <yichenz@chromium.org>
> Commit-Queue: Thomas Anderson <thomasanderson@chromium.org>
> Cr-Commit-Position: refs/heads/main@{#1673803}
Bug: 542165218
Fixed: 540289900
Change-Id: I9fff8f89924244a924d2daa292b98c9f1e6985e2
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8225184
Auto-Submit: Thomas Anderson <thomasanderson@chromium.org>
Reviewed-by: Yichen Zhou <yichenz@chromium.org>
Commit-Queue: Thomas Anderson <thomasanderson@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1680121}
---
diff --git a/ui/base/cursor/cursor_factory.cc b/ui/base/cursor/cursor_factory.cc
index 70fc2653..ddba1a97 100644
--- a/ui/base/cursor/cursor_factory.cc
+++ b/ui/base/cursor/cursor_factory.cc
@@ -8,6 +8,7 @@
#include "base/check.h"
#include "base/check_op.h"
+#include "base/files/file_path.h"
#include "base/memory/scoped_refptr.h"
#include "base/notimplemented.h"
#include "base/notreached.h"
@@ -99,6 +100,19 @@
#if BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS)
+bool IsValidCursorThemeName(std::string_view name) {
+ if (name.empty() || name == ".") {
+ return false;
+ }
+ base::FilePath theme_path(name);
+ return !theme_path.IsAbsolute() && !theme_path.ReferencesParent() &&
+ theme_path.BaseName() == theme_path;
+}
+
+bool IsValidCursorThemeSize(int size) {
+ return size >= 0 && size <= 512;
+}
+
// Returns a cursor name compatible with either X11 or the FreeDesktop.org
// cursor spec ([1] and [2]), followed by fallbacks that can work as
// replacements in some environments where the original may not be available
diff --git a/ui/base/cursor/cursor_factory.h b/ui/base/cursor/cursor_factory.h
index abcc292..3fcfcdf 100644
--- a/ui/base/cursor/cursor_factory.h
+++ b/ui/base/cursor/cursor_factory.h
@@ -7,6 +7,7 @@
#include <optional>
#include <string>
+#include <string_view>
#include <vector>
#include "base/component_export.h"
@@ -96,6 +97,12 @@
#if BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS)
COMPONENT_EXPORT(UI_BASE_CURSOR)
std::vector<std::string> CursorNamesFromType(mojom::CursorType type);
+
+COMPONENT_EXPORT(UI_BASE_CURSOR)
+bool IsValidCursorThemeName(std::string_view name);
+
+COMPONENT_EXPORT(UI_BASE_CURSOR)
+bool IsValidCursorThemeSize(int size);
#endif
} // namespace ui
diff --git a/ui/base/cursor/cursor_unittest.cc b/ui/base/cursor/cursor_unittest.cc
index 446a689..83834f8b 100644
--- a/ui/base/cursor/cursor_unittest.cc
+++ b/ui/base/cursor/cursor_unittest.cc
@@ -4,8 +4,10 @@
#include "ui/base/cursor/cursor.h"
+#include "build/build_config.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/skia/include/core/SkBitmap.h"
+#include "ui/base/cursor/cursor_factory.h"
#include "ui/base/cursor/mojom/cursor_type.mojom-shared.h"
#include "ui/gfx/geometry/point.h"
#include "ui/gfx/image/image_unittest_util.h"
@@ -70,5 +72,27 @@
EXPECT_EQ(gfx::Point(4, 6), cursor.custom_hotspot());
}
+#if BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS)
+TEST(CursorTest, IsValidCursorThemeName) {
+ EXPECT_TRUE(IsValidCursorThemeName("Adwaita"));
+ EXPECT_TRUE(IsValidCursorThemeName("DMZ-White"));
+ EXPECT_FALSE(IsValidCursorThemeName(""));
+ EXPECT_FALSE(IsValidCursorThemeName("."));
+ EXPECT_FALSE(IsValidCursorThemeName("../invalid"));
+ EXPECT_FALSE(IsValidCursorThemeName("/absolute/invalid"));
+ EXPECT_FALSE(IsValidCursorThemeName("sub/dir"));
+ EXPECT_FALSE(IsValidCursorThemeName("../../../../tmp/evil"));
+}
+
+TEST(CursorTest, IsValidCursorThemeSize) {
+ EXPECT_TRUE(IsValidCursorThemeSize(0));
+ EXPECT_TRUE(IsValidCursorThemeSize(16));
+ EXPECT_TRUE(IsValidCursorThemeSize(24));
+ EXPECT_TRUE(IsValidCursorThemeSize(512));
+ EXPECT_FALSE(IsValidCursorThemeSize(-1));
+ EXPECT_FALSE(IsValidCursorThemeSize(513));
+}
+#endif
+
} // namespace
} // namespace ui
diff --git a/ui/base/x/x11_cursor_loader.cc b/ui/base/x/x11_cursor_loader.cc
index ad5feb08..9a88782 100644
--- a/ui/base/x/x11_cursor_loader.cc
+++ b/ui/base/x/x11_cursor_loader.cc
@@ -33,6 +33,7 @@
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "base/time/time.h"
+#include "ui/base/cursor/cursor_factory.h"
#include "ui/base/x/x11_util.h"
#include "ui/gfx/x/atom_cache.h"
#include "ui/gfx/x/connection.h"
@@ -153,9 +154,7 @@
}
bool IsValidCursorThemeName(const std::string& theme) {
- base::FilePath theme_path(theme);
- return !theme.empty() && theme != "." && !theme_path.IsAbsolute() &&
- !theme_path.ReferencesParent() && theme_path.BaseName() == theme_path;
+ return ui::IsValidCursorThemeName(theme);
}
scoped_refptr<base::RefCountedMemory> ReadCursorFromThemeImpl(
diff --git a/ui/gtk/gtk_ui.cc b/ui/gtk/gtk_ui.cc
index a688787..f414351 100644
--- a/ui/gtk/gtk_ui.cc
+++ b/ui/gtk/gtk_ui.cc
@@ -35,6 +35,7 @@
#include "third_party/skia/include/core/SkBitmap.h"
#include "third_party/skia/include/core/SkColor.h"
#include "third_party/skia/include/core/SkShader.h"
+#include "ui/base/cursor/cursor_factory.h"
#include "ui/base/glib/glib_cast.h"
#include "ui/base/ime/input_method.h"
#include "ui/base/ime/linux/fake_input_method_context.h"
@@ -370,6 +371,8 @@
g_object_set(settings, "gtk-modules", "", nullptr);
SanitizeIconThemeName();
SanitizeThemeName();
+ SanitizeCursorThemeName();
+ SanitizeCursorThemeSize();
InstallGtkSettingsInterceptor();
if (!GtkCheckVersion(4)) {
@@ -857,6 +860,26 @@
return false;
}
+bool GtkUi::SanitizeCursorThemeName() {
Regression Test / PoC
diff --git a/ui/base/cursor/cursor_unittest.cc b/ui/base/cursor/cursor_unittest.cc
index 446a689..83834f8b 100644
--- a/ui/base/cursor/cursor_unittest.cc
+++ b/ui/base/cursor/cursor_unittest.cc
@@ -4,8 +4,10 @@
#include "ui/base/cursor/cursor.h"
+#include "build/build_config.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/skia/include/core/SkBitmap.h"
+#include "ui/base/cursor/cursor_factory.h"
#include "ui/base/cursor/mojom/cursor_type.mojom-shared.h"
#include "ui/gfx/geometry/point.h"
#include "ui/gfx/image/image_unittest_util.h"
@@ -70,5 +72,27 @@
EXPECT_EQ(gfx::Point(4, 6), cursor.custom_hotspot());
}
+#if BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS)
+TEST(CursorTest, IsValidCursorThemeName) {
+ EXPECT_TRUE(IsValidCursorThemeName("Adwaita"));
+ EXPECT_TRUE(IsValidCursorThemeName("DMZ-White"));
+ EXPECT_FALSE(IsValidCursorThemeName(""));
+ EXPECT_FALSE(IsValidCursorThemeName("."));
+ EXPECT_FALSE(IsValidCursorThemeName("../invalid"));
+ EXPECT_FALSE(IsValidCursorThemeName("/absolute/invalid"));
+ EXPECT_FALSE(IsValidCursorThemeName("sub/dir"));
+ EXPECT_FALSE(IsValidCursorThemeName("../../../../tmp/evil"));
+}
+
+TEST(CursorTest, IsValidCursorThemeSize) {
+ EXPECT_TRUE(IsValidCursorThemeSize(0));
+ EXPECT_TRUE(IsValidCursorThemeSize(16));
+ EXPECT_TRUE(IsValidCursorThemeSize(24));
+ EXPECT_TRUE(IsValidCursorThemeSize(512));
+ EXPECT_FALSE(IsValidCursorThemeSize(-1));
+ EXPECT_FALSE(IsValidCursorThemeSize(513));
+}
+#endif
+
} // namespace
} // namespace ui
diff --git a/ui/gtk/gtk_util_unittest.cc b/ui/gtk/gtk_util_unittest.cc
index ba116960..933b634f9 100644
--- a/ui/gtk/gtk_util_unittest.cc
+++ b/ui/gtk/gtk_util_unittest.cc
@@ -17,8 +17,10 @@
TEST(GtkUtilTest, IsValidThemeName) {
EXPECT_TRUE(IsValidThemeName(ThemeProperty::kThemeName, "Adwaita"));
EXPECT_TRUE(IsValidThemeName(ThemeProperty::kIconThemeName, "hicolor"));
+ EXPECT_TRUE(IsValidThemeName(ThemeProperty::kCursorThemeName, "Adwaita"));
EXPECT_TRUE(IsValidThemeName(ThemeProperty::kKeyThemeName, ""));
EXPECT_FALSE(IsValidThemeName(ThemeProperty::kThemeName, ""));
+ EXPECT_FALSE(IsValidThemeName(ThemeProperty::kCursorThemeName, ""));
EXPECT_FALSE(IsValidThemeName(ThemeProperty::kThemeName, "../invalid"));
EXPECT_FALSE(
IsValidThemeName(ThemeProperty::kThemeName, "/absolute/invalid"));
@@ -28,6 +30,7 @@
TEST(GtkUtilTest, GetThemeFallback) {
EXPECT_STREQ(GetThemeFallback(ThemeProperty::kIconThemeName), "hicolor");
EXPECT_STREQ(GetThemeFallback(ThemeProperty::kThemeName), "Adwaita");
+ EXPECT_STREQ(GetThemeFallback(ThemeProperty::kCursorThemeName), "Adwaita");
EXPECT_EQ(GetThemeFallback(ThemeProperty::kKeyThemeName), nullptr);
}
@@ -90,4 +93,31 @@
EXPECT_EQ(observed_theme_name, "hicolor");
}
+TEST_F(GtkUtilInterceptorTest, CursorThemeNamesSanitizedAtWriteTime) {
+ GtkSettings* settings = GetDefaultGtkSettings();
+ ASSERT_TRUE(settings);
+
+ std::string observed_theme_name;
+ auto callback = base::BindRepeating(
+ [](std::string* out_str, GtkSettings* settings, GParamSpec* pspec) {
+ gchar* name = nullptr;
+ g_object_get(settings, "gtk-cursor-theme-name", &name, nullptr);
+ if (name) {
+ *out_str = name;
+ g_free(name);
+ }
+ },
+ base::Unretained(&observed_theme_name));
+
+ ScopedGSignal signal(settings, "notify::gtk-cursor-theme-name", callback);
+
+ // Set to an invalid value (path traversal)
+ g_object_set(settings, "gtk-cursor-theme-name",
+ "../../../../tmp/w8_evil_cursor", nullptr);
+
+ // The interceptor should have triggered and sanitized the cursor theme name
+ // to "Adwaita" before the notify callback ran!
+ EXPECT_EQ(observed_theme_name, "Adwaita");
+}
+
} // namespace gtk
Original Bug Report
Incomplete fix of CVE-2026-15769: gtk-cursor-theme-name/size unsanitized and Wayland cursor load has no theme-name validation
Report description
Incomplete fix of CVE-2026-15769: gtk-cursor-theme-name/size unsanitized and Wayland cursor load has no theme-name validation
Bug location
Where do you want to report your vulnerability?
Chrome VRP – Report security issues affecting the Chrome browser. See program rules
Which URL (or repository) have you found the vulnerability in?
https://chromium.googlesource.com/chromium/src/+/refs/heads/main/ui/gtk/gtk_util.cc
The problem
Please describe the technical details of the vulnerability
VULNERABILITY
Incomplete fix of CVE-2026-15769. The M150 fix installs a write-time interceptor on GTK settings (GtkSettingsSetProperty in ui/gtk/gtk_util.cc) that sanitizes theme names, but it covers only three properties (gtk-theme-name, gtk-icon-theme-name, gtk-key-theme-name). gtk-cursor-theme-name and gtk-cursor-theme-size are not on the list, so writes to them pass through raw.
On the load side, the X11 cursor loader validates the theme name with IsValidCursorThemeName (ui/base/x/x11_cursor_loader.cc), which rejects absolute paths and parent references. The Wayland cursor load (ui/ozone/platform/wayland/host/wayland_cursor_factory.cc) stores the raw string and passes it to wl_cursor_theme_load with no validation. libwayland-cursor concatenates the name into the XCURSOR search path (<dir>/<theme>/cursors/<shape>) with no ../ rejection, so a name like ../../../../tmp/evil resolves outside the icons root.
ROOT CAUSE
Two gaps that line up on the Wayland path:
-
Write time: GtkSettingsSetProperty special-cases only the three properties; gtk-cursor-theme-name and gtk-cursor-theme-size fall through to the raw g_orig_set_property.
-
Load time: the Wayland sink does not call IsValidCursorThemeName (the X11 sink does). The raw name reaches wl_cursor_theme_load, and libwayland-cursor resolves it into a filesystem path with no traversal rejection.
The result is a dual-path residual: the same spoofed-settings hop that CVE-2026-15769 was about still forces the browser process to open and parse an attacker-controlled XCursor theme tree on Wayland. X11 is hardened (verified negative control below); Wayland is not.
WHY THIS IS A NEW BUG (and not the fixed CVE)
CVE-2026-15769’s fix rewrote the three listed properties and hardened the X11 cursor sink. It did not touch gtk-cursor-theme-name or gtk-cursor-theme-size, and it did not add validation to the Wayland load. The cursor row has neither half of the fix. Incomplete fixes are treated as new bugs on their own merits; this is the omitted cursor half of that fix. Do not re-file the three fixed properties.
AFFECTED VERSIONS
Chromium main (source verified 2026-07-26): the interceptor omission and the Wayland sink are both present. Chrome for Testing 150.0.7871.186 (post-15769): X11 sink confirmed hardened, confirming the cursor property is the residual. Linux Wayland is the affected configuration; on X11 the traversal is blocked at the load sink.
REPRODUCTION
Two independent measurements, both in the lab and both reproducible.
- The write-time gap (pure GtkSettings, no Chrome needed): set gtk-cursor-theme-name to ../../../../tmp/w8_evil_cursor with g_object_set and read it back. It is retained verbatim, while the interceptor-rewritten properties are forced to Adwaita. From evidence_gtk_property.txt:
BEFORE gtk-cursor-theme-name=(null) AFTER gtk-cursor-theme-name=../../../../tmp/w8_evil_cursor AFTER gtk-theme-name (pure GTK)=../../../invalid-theme NOTE: Chromium interceptor rewrites theme-name to Adwaita; cursor-theme-name has NO interceptor entry
- The load-time path resolution (libwayland-cursor semantics): a name of ../../../../tmp/w8_evil_cursor under /usr/share/icons resolves and opens outside the icons root. From the path-sink harness (evidence_path_sink.txt):
openat(…, “/usr/share/icons/../../../../tmp/w8_evil_cursor/cursors/left_ptr”, O_RDONLY) = 3 (followed by a read of the planted content)
The X11 negative control: on Chrome for Testing 150 (post-fix), the X11 sink rejects the same traversal name (IsValidCursorThemeName), so the gap is specific to the Wayland load.
EVIDENCE
evidence_gtk_property.txt: the interceptor gap (cursor property retained raw, theme-name rewritten).
evidence_path_sink.txt: the libwayland-cursor path concatenation opening a traversal path outside the icons root.
The companion consequence: this gap is the reachability for the out-of-bounds write in Chromium issue 540280769 (the libwayland-cursor pool integer overflow), because the unsanitized theme name and size feed an attacker-chosen theme to the vulnerable allocator. Reported there as the P1 chain.
ATTACHED FILES
gtk_property_gap.c - sets gtk-cursor-theme-name to a traversal value and reads it back wayland_path_sink.c - demonstrates libwayland-cursor resolving a traversal name to an open evidence_gtk_property.txt - the interceptor gap output evidence_path_sink.txt - the path resolution openat proof
Impact analysis
WHO CAN EXPLOIT IT
An attacker who can write to the desktop cursor-theme settings that Chrome reads. In the Chrome threat model this is a compromised renderer or GPU/display process, which acts as the display client and can act as a hostile settings source (the CVE-2026-15769 hop class). The theme name and size are fully attacker-controlled.
WHAT THEY GAIN
The browser process, which is outside the renderer sandbox, opens and parses an attacker-controlled XCursor theme tree on Wayland. On X11 this is blocked at the load sink; on Wayland it is not, and libwayland-cursor resolves traversal names outside the icons root. This is a browser-process filesystem open and parse of attacker bytes, in the same impact class CVE-2026-15769 was rated High for.
The direct impact is an incomplete fix of a shipped High CVE that leaves a browser-process attack surface open through the same settings-spoof hop the CVE addressed. It is also the reachability step for the memory-corruption chain reported in Chromium issue 540280769 (the libwayland-cursor pool integer overflow), where the unsanitized theme name and size feed an attacker-chosen theme to a vulnerable allocator that then performs an out-of-bounds write in the browser process.
This is a logic and missing-validation bug, not memory corruption in its own right; its severity comes from the incomplete fix of a High-rated issue and from being the entry point to the demonstrated pool-overflow chain.
SUGGESTED FIX
Extend GtkSettingsSetProperty to handle gtk-cursor-theme-name with the same path rules as IsValidThemeName / IsValidCursorThemeName (reject empty, absolute, parent-referencing, and multi-component names) and rewrite to a safe fallback, and apply equivalent bounds validation to gtk-cursor-theme-size. Also validate the name in the Wayland cursor load before wl_cursor_theme_load, matching the X11 sink as defense in depth. Unify the validators in a shared helper so X11, Wayland, and the GTK interceptor cannot drift apart again. This is the second drift in this series.
REFERENCE
Chromium issue 540280769: the pool integer overflow this gap reaches. CVE-2026-15769: the incomplete parent fix (Chrome 150.0.7871.125).
The cause
What version of Chrome have you found the security issue in?
150.0.7871.186 [stable]
Is the security issue related to a crash?
No, it is not related to a crash.
Choose the type of vulnerability
Permissions Bypass
How would you like to be publicly acknowledged for your report?
Fabian Wahle (Hap Security)