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 <[email protected]> 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 <[email protected]> > > Commit-Queue: Thomas Anderson <[email protected]> > > 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 <[email protected]> > Commit-Queue: Thomas Anderson <[email protected]> > 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 <[email protected]> Reviewed-by: Yichen Zhou <[email protected]> Commit-Queue: Thomas Anderson <[email protected]> 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)