CVE-2026-19177
Overview
Background
- `GtkSettings` cursor properties
gtk-cursor-theme-nameandgtk-cursor-theme-sizeare GTK settings that come from the desktop session, not from web content, and Chrome reads them to choose the cursor theme it loads on Linux.- Cursor theme lookup
- a cursor theme name gets turned into a directory lookup under the XCursor search paths, so a name like
../../../../tmp/evilcan move the lookup outside the intended icon directories. - Settings interceptor in `ui/gtk/gtk_util.cc`
- Chrome hooks GTK’s set-property path (
g_orig_set_propertyis the saved original), so theme names are checked when they are written, before GTK sendsnotify::callbacks. - `wl_cursor_theme_load`
- a
libwayland-cursorfunction thatWaylandCursorFactorycalls with a theme name (ornullptrfor the default) and a pixel size, and that loads the theme’s cursor images into shared memory.
Root Cause Analysis
Before the patch, Chrome already rejected unsafe GTK theme, icon-theme and key-theme names, and the X11 cursor loader had its own IsValidCursorThemeName check. The GTK cursor-theme name and size had no such check, and neither did the Wayland cursor path. GtkUi::OnCursorThemeNameChanged and GtkUi::OnCursorThemeSizeChanged read the values back from GtkSettings and passed them on unchecked. WaylandCursorFactory::OnCursorThemeNameChanged stored any non-empty name in name_, and the theme-load code later passed name_.c_str() straight to wl_cursor_theme_load.
As a result, an absolute path, a name containing .., or a name with subdirectories could make the cursor loader read theme files from an attacker-chosen directory. OnCursorThemeSizeChanged also stored any integer as size_, with no range check.
The fix moves the existing path-safety rules into shared helpers ui::IsValidCursorThemeName (non-empty, not ., not absolute, no parent references, a single path component) and ui::IsValidCursorThemeSize (0–512). It then checks at every layer: GTK writes are sanitized to Adwaita/24, the GTK change handlers sanitize and return early, and Wayland ignores invalid names and sizes and passes nullptr (the default theme) to wl_cursor_theme_load when the stored name is invalid. The diff is cut off inside the new GTK interceptor unit test, but all the core production changes appear before that point.
wl_cursor_theme_load.Attack Path
- Control the desktop cursor setting
The attacker sets
gtk-cursor-theme-nameto a path-traversing value such as../../../../tmp/<dir>(the form used in the regression test), or setsgtk-cursor-theme-sizeto an out-of-range integer, through whatever session-level settings source GTK reads. - GTK passes the value to Chrome
Before the fix, the
GtkSettingsinterceptor inui/gtk/gtk_util.cconly handled theme, icon-theme and key-theme names, so the write went through andGtkUi::OnCursorThemeNameChanged/GtkUi::OnCursorThemeSizeChangedforwarded it unchecked. - Wayland stores the name and size
WaylandCursorFactory::OnCursorThemeNameChangedchecked only that the name was non-empty before storing it inname_, andOnCursorThemeSizeChangedstoredsize_with no bounds. - Theme loaded from an attacker directory
On the next theme load,
wl_cursor_theme_loadreceived the unchecked name and the scaled size, so it read cursor files from a directory outside the standard theme paths. Any memory-safety consequence of parsing crafted cursor files or using an extreme size inside the cursor library is (inferred) and not shown by the patch.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifui/gtk/gtk_ui.cc |
modified | |
ifui/gtk/gtk_util.cc |
modified | |
TESTui/base/cursor/cursor_unittest.cc |
modified | |
TESTui/gtk/gtk_util_unittest.cc |
modified | |
TEST_Fui/gtk/gtk_util_unittest.cc |
modified | |
ifui/gtk/gtk_util_unittest.cc |
modified |
Files Changed
ui/base/cursor/cursor_unittest.ccui/gtk/gtk_util_unittest.ccui/base/cursor/cursor_factory.ccui/gtk/gtk_ui.hui/base/x/x11_cursor_loader.ccui/gtk/gtk_util.hui/base/cursor/cursor_factory.hui/ozone/platform/wayland/host/wayland_cursor_factory.ccui/gtk/gtk_util.ccui/gtk/gtk_ui.cc
Audit Directions
- Desktop/environment strings used as path componentsLook at other Linux integration points (icon, sound, font, key or cursor themes, XDG settings, portal values) where a session-supplied name is joined into a filesystem path, and confirm each one goes through a basename-only validator like
ui::IsValidCursorThemeName. - Validation applied on one backend onlyThe X11 cursor loader already had this check while GTK and Wayland did not. Compare sibling Ozone backends (X11, Wayland, headless) and toolkit layers (GTK3/GTK4, QT) for checks that exist on one path but are missing on another.
- Numeric settings passed to native librariesLook for sizes, scales or counts from desktop settings that are multiplied (for example through
GetScaledSize) and passed tolibwayland-cursor,libXcursoror similar native code without bounds, where overflow or very large allocations could follow.
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/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() { + std::string theme = GetCursorThemeName(); + if (!IsValidThemeName(ThemeProperty::kCursorThemeName, theme.c_str())) { + g_object_set(gtk_settings_get_default(), "gtk-cursor-theme-name", "Adwaita", + nullptr); + return true; + } + return false; +} + +bool GtkUi::SanitizeCursorThemeSize() { + int size = GetCursorThemeSize(); + if (!ui::IsValidCursorThemeSize(size)) { + g_object_set(gtk_settings_get_default(), "gtk-cursor-theme-size", 24, + nullptr); + return true; + } + return false; +} + void GtkUi::OnKeyThemeNameChanged(GtkSettings* settings, GtkParamSpec* param) { SanitizeKeyThemeName(); } @@ -923,6 +946,9 @@ void GtkUi::OnCursorThemeNameChanged(GtkSettings* settings, GtkParamSpec* param) { + if (SanitizeCursorThemeName()) { + return; + } std::string cursor_theme_name = GetCursorThemeName(); if (cursor_theme_name.empty()) { return; @@ -934,6 +960,9 @@ void GtkUi::OnCursorThemeSizeChanged(GtkSettings* settings, GtkParamSpec* param) { + if (SanitizeCursorThemeSize()) { + return; + } int cursor_theme_size = GetCursorThemeSize(); if (!cursor_theme_size) { return; diff --git a/ui/gtk/gtk_ui.h b/ui/gtk/gtk_ui.h index d017490..244a677 100644 --- a/ui/gtk/gtk_ui.h +++ b/ui/gtk/gtk_ui.h @@ -130,6 +130,14 @@ // Returns true if the setting was modified. bool SanitizeKeyThemeName(); + // Sanitizes the "gtk-cursor-theme-name" setting in GtkSettings if it is + // unsafe. Returns true if the setting was modified. + bool SanitizeCursorThemeName(); + + // Sanitizes the "gtk-cursor-theme-size" setting in GtkSettings if it is + // unsafe. Returns true if the setting was modified. + bool SanitizeCursorThemeSize(); + void OnKeyThemeNameChanged(GtkSettings* settings, GtkParamSpec* param); void OnCursorThemeNameChanged(GtkSettings* settings, GtkParamSpec* param); diff --git a/ui/gtk/gtk_util.cc b/ui/gtk/gtk_util.cc index 8f35e5ad..adc86803 100644 --- a/ui/gtk/gtk_util.cc +++ b/ui/gtk/gtk_util.cc @@ -22,6 +22,7 @@ #include "third_party/skia/include/core/SkBitmap.h" #include "ui/aura/window_tree_host.h" #include "ui/base/accelerators/accelerator.h" +#include "ui/base/cursor/cursor_factory.h" #include "ui/gfx/color_palette.h" #include "ui/gfx/color_utils.h" #include "ui/gfx/geometry/size.h" @@ -769,9 +770,7 @@ if (theme_str.empty()) { return is_key_theme; } - base::FilePath theme_path(theme_str); - return theme_str != "." && !theme_path.IsAbsolute() && - !theme_path.ReferencesParent() && theme_path.BaseName() == theme_path; + return ui::IsValidCursorThemeName(theme_str); } const char* GetThemeFallback(ThemeProperty property) { @@ -779,6 +778,7 @@ case ThemeProperty::kIconThemeName: return "hicolor"; case ThemeProperty::kThemeName: + case ThemeProperty::kCursorThemeName: return "Adwaita"; case ThemeProperty::kKeyThemeName: return nullptr; @@ -807,6 +807,18 @@ property = ThemeProperty::kIconThemeName; } else if (prop_name == "gtk-key-theme-name") { property = ThemeProperty::kKeyThemeName; + } else if (prop_name == "gtk-cursor-theme-name") { + property = ThemeProperty::kCursorThemeName; + } else if (prop_name == "gtk-cursor-theme-size") { + int size = g_value_get_int(value); + if (!ui::IsValidCursorThemeSize(size)) { + GValue sanitized_value = G_VALUE_INIT; + g_value_init(&sanitized_value, G_TYPE_INT); + g_value_set_int(&sanitized_value, 24); + g_orig_set_property(object, property_id, &sanitized_value, pspec); + g_value_unset(&sanitized_value); + return; + } } if (property) { const gchar* name = g_value_get_string(value); diff --git a/ui/gtk/gtk_util.h b/ui/gtk/gtk_util.h index c4c3830..31ea23e 100644 --- a/ui/gtk/gtk_util.h +++ b/ui/gtk/gtk_util.h @@ -204,6 +204,7 @@ kThemeName, kIconThemeName, kKeyThemeName, + kCursorThemeName, }; // Returns true if `theme` is a safe, valid theme name for `property`. diff --git a/ui/ozone/platform/wayland/host/wayland_cursor_factory.cc b/ui/ozone/platform/wayland/host/wayland_cursor_factory.cc index 19d66b1f..6558c9a 100644 --- a/ui/ozone/platform/wayland/host/wayland_cursor_factory.cc +++ b/ui/ozone/platform/wayland/host/wayland_cursor_factory.cc @@ -190,6 +190,9 @@ void WaylandCursorFactory::OnCursorThemeNameChanged( const std::string& cursor_theme_name) { CHECK(!cursor_theme_name.empty()); + if (!IsValidCursorThemeName(cursor_theme_name)) { + return; + } if (name_ == cursor_theme_name) return; @@ -199,6 +202,9 @@ } void WaylandCursorFactory::OnCursorThemeSizeChanged(int cursor_theme_size) { + if (!IsValidCursorThemeSize(cursor_theme_size) || cursor_theme_size == 0) { + return; + } size_ = cursor_theme_size; } @@ -244,7 +250,9 @@ FROM_HERE, {base::MayBlock(), base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN}, base::BindOnce( - wl_cursor_theme_load, name_.empty() ? nullptr : name_.c_str(), + wl_cursor_theme_load, + (!name_.empty() && IsValidCursorThemeName(name_)) ? name_.c_str() + : nullptr, GetScaledSize(scale), connection_->buffer_factory()->shm()), base::BindOnce(&WaylandCursorFactory::FinishThemeLoad, weak_factory_.GetWeakPtr(), cache_entry->GetWeakPtr())); 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)); + ... (file diff truncated)
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)