High watch CVSS 8.3 chrome Input Validation 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
8.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient validation of untrusted input in UI
DescriptionInsufficient validation of untrusted input in UI
ComponentUI
Bug ClassInput Validation
Action tierwatch — logic/policy bug — security-relevant, no memory-unsafety
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker540289900
Fix commit128dd53d2480 (chromium/src) +139/-7
CWECWE-20 (Improper input validation)
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:C/C:H/I:H/A:H
CISA KEVNot listed
CreditedFabian Wahle (Hap Security)
Reported2026-07-29 (per vendor advisory)
Fix landed2026-08-15 (public git)
Disclosed2026-08-06

Background

`GtkSettings` cursor properties
gtk-cursor-theme-name and gtk-cursor-theme-size are 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/evil can 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_property is the saved original), so theme names are checked when they are written, before GTK sends notify:: callbacks.
`wl_cursor_theme_load`
a libwayland-cursor function that WaylandCursorFactory calls with a theme name (or nullptr for 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.

Key insight
Chrome treated the desktop-supplied cursor theme name as a trusted string even though it becomes a filesystem path component, and it did not bound the size. The fix applies the same “single safe basename” rule already used for other GTK theme names and for X11 cursors, and adds a 0–512 size bound, at write time, in the change handlers, and just before wl_cursor_theme_load.

Attack Path

  1. Control the desktop cursor setting The attacker sets gtk-cursor-theme-name to a path-traversing value such as ../../../../tmp/<dir> (the form used in the regression test), or sets gtk-cursor-theme-size to an out-of-range integer, through whatever session-level settings source GTK reads.
  2. GTK passes the value to Chrome Before the fix, the GtkSettings interceptor in ui/gtk/gtk_util.cc only handled theme, icon-theme and key-theme names, so the write went through and GtkUi::OnCursorThemeNameChanged/GtkUi::OnCursorThemeSizeChanged forwarded it unchecked.
  3. Wayland stores the name and size WaylandCursorFactory::OnCursorThemeNameChanged checked only that the name was non-empty before storing it in name_, and OnCursorThemeSizeChanged stored size_ with no bounds.
  4. Theme loaded from an attacker directory On the next theme load, wl_cursor_theme_load received 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

An attacker can redirect the browser process’s cursor theme loading to arbitrary on-disk cursor files, or pass it out-of-range sizes, on Linux builds using the GTK UI and/or the Wayland Ozone backend. The input comes from the desktop session’s GTK settings, not from web content. A stock build cannot be reached by a web page; the attacker must already be able to change the user’s desktop/GTK settings and place files on disk (a local, same-session position). The advisory classes this as insufficient validation of untrusted input in UI. The vendor advisory rates this high severity and does not report in-the-wild exploitation; NVD scores it CVSS 8.3. The provided metadata gives no severity or in-the-wild status.

Changed Functions

FunctionChangeNotes
if
ui/gtk/gtk_ui.cc
modified
if
ui/gtk/gtk_util.cc
modified
TEST
ui/base/cursor/cursor_unittest.cc
modified
TEST
ui/gtk/gtk_util_unittest.cc
modified
TEST_F
ui/gtk/gtk_util_unittest.cc
modified
if
ui/gtk/gtk_util_unittest.cc
modified

Files Changed

  • ui/base/cursor/cursor_unittest.cc
  • ui/gtk/gtk_util_unittest.cc
  • ui/base/cursor/cursor_factory.cc
  • ui/gtk/gtk_ui.h
  • ui/base/x/x11_cursor_loader.cc
  • ui/gtk/gtk_util.h
  • ui/base/cursor/cursor_factory.h
  • ui/ozone/platform/wayland/host/wayland_cursor_factory.cc
  • ui/gtk/gtk_util.cc
  • ui/gtk/gtk_ui.cc

Audit Directions

  • Desktop/environment strings used as path components
    Look 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 only
    The 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 libraries
    Look for sizes, scales or counts from desktop settings that are multiplied (for example through GetScaledSize) and passed to libwayland-cursor, libXcursor or similar native code without bounds, where overflow or very large allocations could follow.
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)
Loading diff…

Regression Test / PoC

shipped with the fix
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
Loading diff…

Original Bug Report

reported by [email protected]

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:

  1. 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.

  2. 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.

  1. 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

  1. 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]

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)

View on issue tracker